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

djeedai / bevy_hanabi / 14605212650

22 Apr 2025 09:43PM UTC coverage: 39.856% (-0.04%) from 39.892%
14605212650

push

github

web-flow
Switch `GpuDispatchIndirect` buffers to `GpuBuffer` (#460)

Both the indirect init and indirect update buffers used complex data structures
designed for CPU/GPU interaction. However those buffers are only ever used on
GPU. Switch both of them to use `GpuBuffer`, which doesn't handle any CPU
access and is therefore more simple and optimized (doesn't make any CPU -> GPU
upload copy for example).

Fix a bug where an old buffer for CPU spawners was used after being
reallocated, which caused a panic when submitting the render queue.

4 of 41 new or added lines in 3 files covered. (9.76%)

2 existing lines in 2 files now uncovered.

3043 of 7635 relevant lines covered (39.86%)

17.57 hits per line

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

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

62
use crate::{
63
    asset::{DefaultMesh, EffectAsset},
64
    calc_func_id,
65
    plugin::WithCompiledParticleEffect,
66
    render::{
67
        batch::{BatchInput, EffectDrawBatch, InitAndUpdatePipelineIds},
68
        effect_cache::DispatchBufferIndices,
69
    },
70
    AlphaMode, Attribute, CompiledParticleEffect, EffectProperties, EffectShader, EffectSimulation,
71
    EffectSpawner, ParticleLayout, PropertyLayout, SimulationCondition, TextureLayout,
72
};
73

74
mod aligned_buffer_vec;
75
mod batch;
76
mod buffer_table;
77
mod effect_cache;
78
mod event;
79
mod gpu_buffer;
80
mod property;
81
mod shader_cache;
82
mod sort;
83

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

97
use self::batch::EffectBatch;
98

99
// Size of an indirect index (including both parts of the ping-pong buffer) in
100
// bytes.
101
const INDIRECT_INDEX_SIZE: u32 = 12;
102

103
/// Helper to calculate a hash of a given hashable value.
104
fn calc_hash<H: Hash>(value: &H) -> u64 {
×
105
    let mut hasher = DefaultHasher::default();
×
106
    value.hash(&mut hasher);
×
107
    hasher.finish()
×
108
}
109

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

118
impl BufferBindingSource {
119
    /// Get a binding over the source data.
120
    pub fn binding(&self) -> BindingResource {
×
121
        BindingResource::Buffer(BufferBinding {
×
122
            buffer: &self.buffer,
×
123
            offset: self.offset as u64 * 4,
×
124
            size: Some(self.size.into()),
×
125
        })
126
    }
127
}
128

129
impl PartialEq for BufferBindingSource {
130
    fn eq(&self, other: &Self) -> bool {
×
131
        self.buffer.id() == other.buffer.id()
×
132
            && self.offset == other.offset
×
133
            && self.size == other.size
×
134
    }
135
}
136

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

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

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

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

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

200
impl Default for GpuSimParams {
201
    fn default() -> Self {
×
202
        Self {
203
            delta_time: 0.04,
204
            time: 0.0,
205
            virtual_delta_time: 0.04,
206
            virtual_time: 0.0,
207
            real_delta_time: 0.04,
208
            real_time: 0.0,
209
            num_effects: 0,
210
        }
211
    }
212
}
213

214
impl From<SimParams> for GpuSimParams {
215
    #[inline]
216
    fn from(src: SimParams) -> Self {
×
217
        Self::from(&src)
×
218
    }
219
}
220

221
impl From<&SimParams> for GpuSimParams {
222
    fn from(src: &SimParams) -> Self {
×
223
        Self {
224
            delta_time: src.delta_time,
×
225
            time: src.time as f32,
×
226
            virtual_delta_time: src.virtual_delta_time,
×
227
            virtual_time: src.virtual_time as f32,
×
228
            real_delta_time: src.real_delta_time,
×
229
            real_time: src.real_time as f32,
×
230
            ..default()
231
        }
232
    }
233
}
234

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

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

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

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

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

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

305
impl<T: ShaderType> StorageType for T {
306
    fn aligned_size(alignment: u32) -> NonZeroU64 {
14✔
307
        NonZeroU64::new(T::min_size().get().next_multiple_of(alignment as u64)).unwrap()
14✔
308
    }
309

310
    fn padding_code(alignment: u32) -> String {
6✔
311
        let aligned_size = T::aligned_size(alignment);
6✔
312
        trace!(
6✔
313
            "Aligning {} to {} bytes as device limits requires. Orignal size: {} bytes. Aligned size: {} bytes.",
×
314
            std::any::type_name::<T>(),
×
315
            alignment,
×
316
            T::min_size().get(),
×
317
            aligned_size
×
318
        );
319

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

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

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

376
impl Default for GpuDispatchIndirect {
377
    fn default() -> Self {
×
378
        Self { x: 0, y: 1, z: 1 }
379
    }
380
}
381

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

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

437
    /// Particle stride, in number of u32.
438
    pub particle_stride: u32,
439
    /// Offset from the particle start to the first sort key, in number of u32.
440
    pub sort_key_offset: u32,
441
    /// Offset from the particle start to the second sort key, in number of u32.
442
    pub sort_key2_offset: u32,
443

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

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

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

474
impl InitFillDispatchQueue {
475
    /// Clear the queue.
476
    #[inline]
477
    pub fn clear(&mut self) {
×
478
        self.queue.clear();
×
479
        self.submitted_queue_index = None;
×
480
    }
481

482
    /// Check if the queue is empty.
483
    #[inline]
484
    pub fn is_empty(&self) -> bool {
×
485
        self.queue.is_empty()
×
486
    }
487

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

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

508
        // Sort by source. We can only batch if the destination is also contiguous, so
509
        // we can check with a linear walk if the source is already sorted.
510
        self.queue
3✔
511
            .sort_unstable_by_key(|item| item.global_child_index);
9✔
512

513
        let mut fill_queue = GpuBufferOperationQueue::new();
514

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

591
        debug_assert!(self.submitted_queue_index.is_none());
3✔
592
        if !fill_queue.operation_queue.is_empty() {
6✔
593
            self.submitted_queue_index = Some(gpu_buffer_operations.submit(fill_queue));
3✔
594
        }
595
    }
596
}
597

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

616
impl FromWorld for DispatchIndirectPipeline {
617
    fn from_world(world: &mut World) -> Self {
×
618
        let render_device = world.get_resource::<RenderDevice>().unwrap();
×
619

620
        // Copy the indirect pipeline shaders to self, because we can't access anything
621
        // else during pipeline specialization.
622
        let (indirect_shader_noevent, indirect_shader_events) = {
×
623
            let effects_meta = world.get_resource::<EffectsMeta>().unwrap();
×
624
            (
625
                effects_meta.indirect_shader_noevent.clone(),
×
626
                effects_meta.indirect_shader_events.clone(),
×
627
            )
628
        };
629

630
        let storage_alignment = render_device.limits().min_storage_buffer_offset_alignment;
×
631
        let render_effect_metadata_size = GpuEffectMetadata::aligned_size(storage_alignment);
×
632
        let spawner_min_binding_size = GpuSpawnerParams::aligned_size(storage_alignment);
×
633

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

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

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

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

719
        Self {
720
            sim_params_bind_group_layout,
721
            effect_metadata_bind_group_layout,
722
            spawner_bind_group_layout,
723
            child_infos_bind_group_layout,
724
            indirect_shader_noevent,
725
            indirect_shader_events,
726
        }
727
    }
728
}
729

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

740
impl SpecializedComputePipeline for DispatchIndirectPipeline {
741
    type Key = DispatchIndirectPipelineKey;
742

743
    fn specialize(&self, key: Self::Key) -> ComputePipelineDescriptor {
×
744
        trace!(
×
745
            "Specializing indirect pipeline (has_events={})",
×
746
            key.has_events
747
        );
748

749
        let mut shader_defs = Vec::with_capacity(2);
×
750
        // Spawner struct needs to be defined with padding, because it's bound as an
751
        // array
752
        shader_defs.push("SPAWNER_PADDING".into());
×
753
        if key.has_events {
×
754
            shader_defs.push("HAS_GPU_SPAWN_EVENTS".into());
×
755
        }
756

757
        let mut layout = Vec::with_capacity(4);
×
758
        layout.push(self.sim_params_bind_group_layout.clone());
×
759
        layout.push(self.effect_metadata_bind_group_layout.clone());
×
760
        layout.push(self.spawner_bind_group_layout.clone());
×
761
        if key.has_events {
×
762
            layout.push(self.child_infos_bind_group_layout.clone());
×
763
        }
764

765
        let label = format!(
×
766
            "hanabi:compute_pipeline:dispatch_indirect{}",
767
            if key.has_events {
×
768
                "_events"
×
769
            } else {
770
                "_noevent"
×
771
            }
772
        );
773

774
        ComputePipelineDescriptor {
775
            label: Some(label.into()),
×
776
            layout,
777
            shader: if key.has_events {
×
778
                self.indirect_shader_events.clone()
779
            } else {
780
                self.indirect_shader_noevent.clone()
781
            },
782
            shader_defs,
783
            entry_point: "main".into(),
×
784
            push_constant_ranges: vec![],
×
785
            zero_initialize_workgroup_memory: false,
786
        }
787
    }
788
}
789

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

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

839
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
840
struct QueuedOperationBindGroupKey {
841
    src_buffer: BufferId,
842
    src_binding_size: Option<NonZeroU32>,
843
    dst_buffer: BufferId,
844
    dst_binding_size: Option<NonZeroU32>,
845
}
846

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

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

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

883
impl GpuBufferOperationQueue {
884
    /// Create a new empty queue.
885
    pub fn new() -> Self {
3✔
886
        Self {
887
            args: vec![],
3✔
888
            operation_queue: vec![],
3✔
889
        }
890
    }
891

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

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

941
    /// Bind groups for the submitted operations.
942
    bind_groups: HashMap<QueuedOperationBindGroupKey, BindGroup>,
943

944
    /// Submitted queues for this frame.
945
    queues: Vec<Vec<QueuedOperation>>,
946
}
947

948
impl FromWorld for GpuBufferOperations {
949
    fn from_world(world: &mut World) -> Self {
1✔
950
        let render_device = world.get_resource::<RenderDevice>().unwrap();
1✔
951
        let align = render_device.limits().min_uniform_buffer_offset_alignment;
1✔
952
        Self::new(align)
1✔
953
    }
954
}
955

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

970
    /// Clear the queue and begin recording operations for a new frame.
971
    pub fn begin_frame(&mut self) {
3✔
972
        self.args_buffer.clear();
3✔
973
        self.bind_groups.clear(); // for now; might consider caching frame-to-frame
3✔
974
        self.queues.clear();
3✔
975
    }
976

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

992
    /// Finish recording operations for this frame, and schedule buffer writes
993
    /// to GPU.
994
    pub fn end_frame(&mut self, device: &RenderDevice, render_queue: &RenderQueue) {
3✔
995
        assert_eq!(
3✔
996
            self.args_buffer.len(),
3✔
997
            self.queues.iter().fold(0, |len, q| len + q.len())
9✔
998
        );
999

1000
        // Upload to GPU buffer
1001
        self.args_buffer.write_buffer(device, render_queue);
3✔
1002
    }
1003

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

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

1101
        if queue.is_empty() {
×
1102
            return;
×
1103
        }
1104

1105
        let mut compute_pass =
×
1106
            render_context
×
1107
                .command_encoder()
1108
                .begin_compute_pass(&ComputePassDescriptor {
×
1109
                    label: compute_pass_label,
×
1110
                    timestamp_writes: None,
×
1111
                });
1112

1113
        let mut prev_op = None;
×
1114
        for qop in queue {
×
1115
            trace!("qop={:?}", qop);
×
1116

1117
            if Some(qop.op) != prev_op {
×
1118
                compute_pass.set_pipeline(utils_pipeline.get_pipeline(qop.op));
×
1119
                prev_op = Some(qop.op);
×
1120
            }
1121

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

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

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

1167
impl FromWorld for UtilsPipeline {
1168
    fn from_world(world: &mut World) -> Self {
×
1169
        let render_device = world.get_resource::<RenderDevice>().unwrap();
×
1170

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

1207
        let pipeline_layout = render_device.create_pipeline_layout(&PipelineLayoutDescriptor {
×
1208
            label: Some("hanabi:pipeline_layout:utils"),
×
1209
            bind_group_layouts: &[&bind_group_layout],
×
1210
            push_constant_ranges: &[],
×
1211
        });
1212

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

1249
        let pipeline_layout_dyn = render_device.create_pipeline_layout(&PipelineLayoutDescriptor {
×
1250
            label: Some("hanabi:pipeline_layout:utils_dyn"),
×
1251
            bind_group_layouts: &[&bind_group_layout_dyn],
×
1252
            push_constant_ranges: &[],
×
1253
        });
1254

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

1281
        let pipeline_layout_no_src =
×
1282
            render_device.create_pipeline_layout(&PipelineLayoutDescriptor {
×
1283
                label: Some("hanabi:pipeline_layout:utils_no_src"),
×
1284
                bind_group_layouts: &[&bind_group_layout_no_src],
×
1285
                push_constant_ranges: &[],
×
1286
            });
1287

1288
        let shader_code = include_str!("vfx_utils.wgsl");
×
1289

1290
        // Resolve imports. Because we don't insert this shader into Bevy' pipeline
1291
        // cache, we don't get that part "for free", so we have to do it manually here.
1292
        let shader_source = {
×
1293
            let mut composer = Composer::default();
×
1294

1295
            let shader_defs = default();
×
1296

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

1311
        debug!("Create utils shader module:\n{}", shader_code);
×
1312
        let shader_module = render_device.create_shader_module(ShaderModuleDescriptor {
×
1313
            label: Some("hanabi:shader:utils"),
×
1314
            source: shader_source,
×
1315
        });
1316

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

1366
        Self {
1367
            bind_group_layout,
1368
            bind_group_layout_dyn,
1369
            bind_group_layout_no_src,
1370
            pipelines: [
×
1371
                zero_pipeline,
1372
                copy_pipeline,
1373
                fill_dispatch_args_pipeline,
1374
                fill_dispatch_args_self_pipeline,
1375
            ],
1376
        }
1377
    }
1378
}
1379

1380
impl UtilsPipeline {
1381
    fn get_pipeline(&self, op: GpuBufferOperationType) -> &ComputePipeline {
×
1382
        match op {
×
1383
            GpuBufferOperationType::Zero => &self.pipelines[0],
×
1384
            GpuBufferOperationType::Copy => &self.pipelines[1],
×
1385
            GpuBufferOperationType::FillDispatchArgs => &self.pipelines[2],
×
1386
            GpuBufferOperationType::FillDispatchArgsSelf => &self.pipelines[3],
×
1387
        }
1388
    }
1389

1390
    fn bind_group_layout(
×
1391
        &self,
1392
        op: GpuBufferOperationType,
1393
        with_dynamic_offsets: bool,
1394
    ) -> &BindGroupLayout {
1395
        if op == GpuBufferOperationType::FillDispatchArgsSelf {
×
1396
            assert!(
×
1397
                !with_dynamic_offsets,
×
1398
                "FillDispatchArgsSelf op cannot use dynamic offset (not implemented)"
×
1399
            );
1400
            &self.bind_group_layout_no_src
×
1401
        } else if with_dynamic_offsets {
×
1402
            &self.bind_group_layout_dyn
×
1403
        } else {
1404
            &self.bind_group_layout
×
1405
        }
1406
    }
1407
}
1408

1409
#[derive(Resource)]
1410
pub(crate) struct ParticlesInitPipeline {
1411
    sim_params_layout: BindGroupLayout,
1412

1413
    // Temporary values passed to specialize()
1414
    // https://github.com/bevyengine/bevy/issues/17132
1415
    /// Layout of the particle@1 bind group this pipeline was specialized with.
1416
    temp_particle_bind_group_layout: Option<BindGroupLayout>,
1417
    /// Layout of the spawner@2 bind group this pipeline was specialized with.
1418
    temp_spawner_bind_group_layout: Option<BindGroupLayout>,
1419
    /// Layout of the metadata@3 bind group this pipeline was specialized with.
1420
    temp_metadata_bind_group_layout: Option<BindGroupLayout>,
1421
}
1422

1423
impl FromWorld for ParticlesInitPipeline {
1424
    fn from_world(world: &mut World) -> Self {
×
1425
        let render_device = world.get_resource::<RenderDevice>().unwrap();
×
1426

1427
        let sim_params_layout = render_device.create_bind_group_layout(
×
1428
            "hanabi:bind_group_layout:update_sim_params",
1429
            // @group(0) @binding(0) var<uniform> sim_params: SimParams;
1430
            &[BindGroupLayoutEntry {
×
1431
                binding: 0,
×
1432
                visibility: ShaderStages::COMPUTE,
×
1433
                ty: BindingType::Buffer {
×
1434
                    ty: BufferBindingType::Uniform,
×
1435
                    has_dynamic_offset: false,
×
1436
                    min_binding_size: Some(GpuSimParams::min_size()),
×
1437
                },
1438
                count: None,
×
1439
            }],
1440
        );
1441

1442
        Self {
1443
            sim_params_layout,
1444
            temp_particle_bind_group_layout: None,
1445
            temp_spawner_bind_group_layout: None,
1446
            temp_metadata_bind_group_layout: None,
1447
        }
1448
    }
1449
}
1450

1451
bitflags! {
1452
    #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1453
    pub struct ParticleInitPipelineKeyFlags: u8 {
1454
        //const CLONE = (1u8 << 0); // DEPRECATED
1455
        const ATTRIBUTE_PREV = (1u8 << 1);
1456
        const ATTRIBUTE_NEXT = (1u8 << 2);
1457
        const CONSUME_GPU_SPAWN_EVENTS = (1u8 << 3);
1458
    }
1459
}
1460

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

1484
impl SpecializedComputePipeline for ParticlesInitPipeline {
1485
    type Key = ParticleInitPipelineKey;
1486

1487
    fn specialize(&self, key: Self::Key) -> ComputePipelineDescriptor {
×
1488
        // We use the hash to correlate the key content with the GPU resource name
1489
        let hash = calc_hash(&key);
×
1490
        trace!("Specializing init pipeline {hash:016X} with key {key:?}");
×
1491

1492
        let mut shader_defs = Vec::with_capacity(4);
×
1493
        if key
×
1494
            .flags
×
1495
            .contains(ParticleInitPipelineKeyFlags::ATTRIBUTE_PREV)
×
1496
        {
1497
            shader_defs.push("ATTRIBUTE_PREV".into());
×
1498
        }
1499
        if key
×
1500
            .flags
×
1501
            .contains(ParticleInitPipelineKeyFlags::ATTRIBUTE_NEXT)
×
1502
        {
1503
            shader_defs.push("ATTRIBUTE_NEXT".into());
×
1504
        }
1505
        let consume_gpu_spawn_events = key
×
1506
            .flags
×
1507
            .contains(ParticleInitPipelineKeyFlags::CONSUME_GPU_SPAWN_EVENTS);
×
1508
        if consume_gpu_spawn_events {
×
1509
            shader_defs.push("CONSUME_GPU_SPAWN_EVENTS".into());
×
1510
        }
1511
        // FIXME - for now this needs to keep in sync with consume_gpu_spawn_events
1512
        if key.parent_particle_layout_min_binding_size.is_some() {
×
1513
            assert!(consume_gpu_spawn_events);
×
1514
            shader_defs.push("READ_PARENT_PARTICLE".into());
×
1515
        } else {
1516
            assert!(!consume_gpu_spawn_events);
×
1517
        }
1518

1519
        // This should always be valid when specialize() is called, by design. This is
1520
        // how we pass the value to specialize() to work around the lack of access to
1521
        // external data.
1522
        // https://github.com/bevyengine/bevy/issues/17132
1523
        let particle_bind_group_layout = self.temp_particle_bind_group_layout.as_ref().unwrap();
×
1524
        assert_eq!(
×
1525
            particle_bind_group_layout.id(),
×
1526
            key.particle_bind_group_layout_id
×
1527
        );
1528
        let spawner_bind_group_layout = self.temp_spawner_bind_group_layout.as_ref().unwrap();
×
1529
        assert_eq!(
×
1530
            spawner_bind_group_layout.id(),
×
1531
            key.spawner_bind_group_layout_id
×
1532
        );
1533
        let metadata_bind_group_layout = self.temp_metadata_bind_group_layout.as_ref().unwrap();
×
1534
        assert_eq!(
×
1535
            metadata_bind_group_layout.id(),
×
1536
            key.metadata_bind_group_layout_id
×
1537
        );
1538

1539
        let label = format!("hanabi:pipeline:init_{hash:016X}");
×
1540
        trace!(
×
1541
            "-> creating pipeline '{}' with shader defs:{}",
×
1542
            label,
×
1543
            shader_defs
×
1544
                .iter()
×
1545
                .fold(String::new(), |acc, x| acc + &format!(" {x:?}"))
×
1546
        );
1547

1548
        ComputePipelineDescriptor {
1549
            label: Some(label.into()),
×
1550
            layout: vec![
×
1551
                self.sim_params_layout.clone(),
1552
                particle_bind_group_layout.clone(),
1553
                spawner_bind_group_layout.clone(),
1554
                metadata_bind_group_layout.clone(),
1555
            ],
1556
            shader: key.shader,
×
1557
            shader_defs,
1558
            entry_point: "main".into(),
×
1559
            push_constant_ranges: vec![],
×
1560
            zero_initialize_workgroup_memory: false,
1561
        }
1562
    }
1563
}
1564

1565
#[derive(Resource)]
1566
pub(crate) struct ParticlesUpdatePipeline {
1567
    sim_params_layout: BindGroupLayout,
1568

1569
    // Temporary values passed to specialize()
1570
    // https://github.com/bevyengine/bevy/issues/17132
1571
    /// Layout of the particle@1 bind group this pipeline was specialized with.
1572
    temp_particle_bind_group_layout: Option<BindGroupLayout>,
1573
    /// Layout of the spawner@2 bind group this pipeline was specialized with.
1574
    temp_spawner_bind_group_layout: Option<BindGroupLayout>,
1575
    /// Layout of the metadata@3 bind group this pipeline was specialized with.
1576
    temp_metadata_bind_group_layout: Option<BindGroupLayout>,
1577
}
1578

1579
impl FromWorld for ParticlesUpdatePipeline {
1580
    fn from_world(world: &mut World) -> Self {
×
1581
        let render_device = world.get_resource::<RenderDevice>().unwrap();
×
1582

1583
        trace!("GpuSimParams: min_size={}", GpuSimParams::min_size());
×
1584
        let sim_params_layout = render_device.create_bind_group_layout(
×
1585
            "hanabi:bind_group_layout:update:particle",
1586
            &[BindGroupLayoutEntry {
×
1587
                binding: 0,
×
1588
                visibility: ShaderStages::COMPUTE,
×
1589
                ty: BindingType::Buffer {
×
1590
                    ty: BufferBindingType::Uniform,
×
1591
                    has_dynamic_offset: false,
×
1592
                    min_binding_size: Some(GpuSimParams::min_size()),
×
1593
                },
1594
                count: None,
×
1595
            }],
1596
        );
1597

1598
        Self {
1599
            sim_params_layout,
1600
            temp_particle_bind_group_layout: None,
1601
            temp_spawner_bind_group_layout: None,
1602
            temp_metadata_bind_group_layout: None,
1603
        }
1604
    }
1605
}
1606

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

1630
impl SpecializedComputePipeline for ParticlesUpdatePipeline {
1631
    type Key = ParticleUpdatePipelineKey;
1632

1633
    fn specialize(&self, key: Self::Key) -> ComputePipelineDescriptor {
×
1634
        // We use the hash to correlate the key content with the GPU resource name
1635
        let hash = calc_hash(&key);
×
1636
        trace!("Specializing update pipeline {hash:016X} with key {key:?}");
×
1637

1638
        let mut shader_defs = Vec::with_capacity(6);
×
1639
        shader_defs.push("EM_MAX_SPAWN_ATOMIC".into());
×
1640
        // ChildInfo needs atomic event_count because all threads append to the event
1641
        // buffer(s) in parallel.
1642
        shader_defs.push("CHILD_INFO_EVENT_COUNT_IS_ATOMIC".into());
×
1643
        if key.particle_layout.contains(Attribute::PREV) {
×
1644
            shader_defs.push("ATTRIBUTE_PREV".into());
×
1645
        }
1646
        if key.particle_layout.contains(Attribute::NEXT) {
×
1647
            shader_defs.push("ATTRIBUTE_NEXT".into());
×
1648
        }
1649
        if key.parent_particle_layout_min_binding_size.is_some() {
×
1650
            shader_defs.push("READ_PARENT_PARTICLE".into());
×
1651
        }
1652
        if key.num_event_buffers > 0 {
×
1653
            shader_defs.push("EMITS_GPU_SPAWN_EVENTS".into());
×
1654
        }
1655

1656
        // This should always be valid when specialize() is called, by design. This is
1657
        // how we pass the value to specialize() to work around the lack of access to
1658
        // external data.
1659
        // https://github.com/bevyengine/bevy/issues/17132
1660
        let particle_bind_group_layout = self.temp_particle_bind_group_layout.as_ref().unwrap();
×
1661
        assert_eq!(
×
1662
            particle_bind_group_layout.id(),
×
1663
            key.particle_bind_group_layout_id
×
1664
        );
1665
        let spawner_bind_group_layout = self.temp_spawner_bind_group_layout.as_ref().unwrap();
×
1666
        assert_eq!(
×
1667
            spawner_bind_group_layout.id(),
×
1668
            key.spawner_bind_group_layout_id
×
1669
        );
1670
        let metadata_bind_group_layout = self.temp_metadata_bind_group_layout.as_ref().unwrap();
×
1671
        assert_eq!(
×
1672
            metadata_bind_group_layout.id(),
×
1673
            key.metadata_bind_group_layout_id
×
1674
        );
1675

1676
        let hash = calc_func_id(&key);
×
1677
        let label = format!("hanabi:pipeline:update_{hash:016X}");
×
1678
        trace!(
×
1679
            "-> creating pipeline '{}' with shader defs:{}",
×
1680
            label,
×
1681
            shader_defs
×
1682
                .iter()
×
1683
                .fold(String::new(), |acc, x| acc + &format!(" {x:?}"))
×
1684
        );
1685

1686
        ComputePipelineDescriptor {
1687
            label: Some(label.into()),
×
1688
            layout: vec![
×
1689
                self.sim_params_layout.clone(),
1690
                particle_bind_group_layout.clone(),
1691
                spawner_bind_group_layout.clone(),
1692
                metadata_bind_group_layout.clone(),
1693
            ],
1694
            shader: key.shader,
×
1695
            shader_defs,
1696
            entry_point: "main".into(),
×
1697
            push_constant_ranges: Vec::new(),
×
1698
            zero_initialize_workgroup_memory: false,
1699
        }
1700
    }
1701
}
1702

1703
#[derive(Resource)]
1704
pub(crate) struct ParticlesRenderPipeline {
1705
    render_device: RenderDevice,
1706
    view_layout: BindGroupLayout,
1707
    material_layouts: HashMap<TextureLayout, BindGroupLayout>,
1708
}
1709

1710
impl ParticlesRenderPipeline {
1711
    /// Cache a material, creating its bind group layout based on the texture
1712
    /// layout.
1713
    pub fn cache_material(&mut self, layout: &TextureLayout) {
×
1714
        if layout.layout.is_empty() {
×
1715
            return;
×
1716
        }
1717

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

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

1757
        self.material_layouts
1758
            .insert(layout.clone(), material_bind_group_layout);
1759
    }
1760

1761
    /// Retrieve a bind group layout for a cached material.
1762
    pub fn get_material(&self, layout: &TextureLayout) -> Option<&BindGroupLayout> {
×
1763
        // Prevent a hash and lookup for the trivial case of an empty layout
1764
        if layout.layout.is_empty() {
×
1765
            return None;
×
1766
        }
1767

1768
        self.material_layouts.get(layout)
×
1769
    }
1770
}
1771

1772
impl FromWorld for ParticlesRenderPipeline {
1773
    fn from_world(world: &mut World) -> Self {
×
1774
        let render_device = world.get_resource::<RenderDevice>().unwrap();
×
1775

1776
        let view_layout = render_device.create_bind_group_layout(
×
1777
            "hanabi:bind_group_layout:render:view@0",
1778
            &[
×
1779
                // @group(0) @binding(0) var<uniform> view: View;
1780
                BindGroupLayoutEntry {
×
1781
                    binding: 0,
×
1782
                    visibility: ShaderStages::VERTEX_FRAGMENT,
×
1783
                    ty: BindingType::Buffer {
×
1784
                        ty: BufferBindingType::Uniform,
×
1785
                        has_dynamic_offset: true,
×
1786
                        min_binding_size: Some(ViewUniform::min_size()),
×
1787
                    },
1788
                    count: None,
×
1789
                },
1790
                // @group(0) @binding(1) var<uniform> sim_params : SimParams;
1791
                BindGroupLayoutEntry {
×
1792
                    binding: 1,
×
1793
                    visibility: ShaderStages::VERTEX_FRAGMENT,
×
1794
                    ty: BindingType::Buffer {
×
1795
                        ty: BufferBindingType::Uniform,
×
1796
                        has_dynamic_offset: false,
×
1797
                        min_binding_size: Some(GpuSimParams::min_size()),
×
1798
                    },
1799
                    count: None,
×
1800
                },
1801
            ],
1802
        );
1803

1804
        Self {
1805
            render_device: render_device.clone(),
×
1806
            view_layout,
1807
            material_layouts: default(),
×
1808
        }
1809
    }
1810
}
1811

1812
#[cfg(all(feature = "2d", feature = "3d"))]
1813
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
1814
enum PipelineMode {
1815
    Camera2d,
1816
    Camera3d,
1817
}
1818

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

1861
#[derive(Clone, Copy, Default, Hash, PartialEq, Eq, Debug)]
1862
pub(crate) enum ParticleRenderAlphaMaskPipelineKey {
1863
    #[default]
1864
    Blend,
1865
    /// Key: USE_ALPHA_MASK
1866
    /// The effect is rendered with alpha masking.
1867
    AlphaMask,
1868
    /// Key: OPAQUE
1869
    /// The effect is rendered fully-opaquely.
1870
    Opaque,
1871
}
1872

1873
impl Default for ParticleRenderPipelineKey {
1874
    fn default() -> Self {
×
1875
        Self {
1876
            shader: Handle::default(),
×
1877
            particle_layout: ParticleLayout::empty(),
×
1878
            mesh_layout: None,
1879
            texture_layout: default(),
×
1880
            local_space_simulation: false,
1881
            alpha_mask: default(),
×
1882
            alpha_mode: AlphaMode::Blend,
1883
            flipbook: false,
1884
            needs_uv: false,
1885
            needs_normal: false,
1886
            ribbons: false,
1887
            #[cfg(all(feature = "2d", feature = "3d"))]
1888
            pipeline_mode: PipelineMode::Camera3d,
1889
            msaa_samples: Msaa::default().samples(),
×
1890
            hdr: false,
1891
        }
1892
    }
1893
}
1894

1895
impl SpecializedRenderPipeline for ParticlesRenderPipeline {
1896
    type Key = ParticleRenderPipelineKey;
1897

1898
    fn specialize(&self, key: Self::Key) -> RenderPipelineDescriptor {
×
1899
        trace!("Specializing render pipeline for key: {key:?}");
×
1900

1901
        trace!("Creating layout for bind group particle@1 of render pass");
×
1902
        let alignment = self
×
1903
            .render_device
×
1904
            .limits()
×
1905
            .min_storage_buffer_offset_alignment;
×
1906
        let spawner_min_binding_size = GpuSpawnerParams::aligned_size(alignment);
×
1907
        let entries = [
×
1908
            // @group(1) @binding(0) var<storage, read> particle_buffer : ParticleBuffer;
1909
            BindGroupLayoutEntry {
×
1910
                binding: 0,
×
1911
                visibility: ShaderStages::VERTEX,
×
1912
                ty: BindingType::Buffer {
×
1913
                    ty: BufferBindingType::Storage { read_only: true },
×
1914
                    has_dynamic_offset: false,
×
1915
                    min_binding_size: Some(key.particle_layout.min_binding_size()),
×
1916
                },
1917
                count: None,
×
1918
            },
1919
            // @group(1) @binding(1) var<storage, read> indirect_buffer : IndirectBuffer;
1920
            BindGroupLayoutEntry {
×
1921
                binding: 1,
×
1922
                visibility: ShaderStages::VERTEX,
×
1923
                ty: BindingType::Buffer {
×
1924
                    ty: BufferBindingType::Storage { read_only: true },
×
1925
                    has_dynamic_offset: false,
×
1926
                    min_binding_size: Some(NonZeroU64::new(INDIRECT_INDEX_SIZE as u64).unwrap()),
×
1927
                },
1928
                count: None,
×
1929
            },
1930
            // @group(1) @binding(2) var<storage, read> spawner : Spawner;
1931
            BindGroupLayoutEntry {
×
1932
                binding: 2,
×
1933
                visibility: ShaderStages::VERTEX,
×
1934
                ty: BindingType::Buffer {
×
1935
                    ty: BufferBindingType::Storage { read_only: true },
×
1936
                    has_dynamic_offset: true,
×
1937
                    min_binding_size: Some(spawner_min_binding_size),
×
1938
                },
1939
                count: None,
×
1940
            },
1941
        ];
1942
        let particle_bind_group_layout = self
×
1943
            .render_device
×
1944
            .create_bind_group_layout("hanabi:bind_group_layout:render:particle@1", &entries[..]);
×
1945

1946
        let mut layout = vec![self.view_layout.clone(), particle_bind_group_layout];
×
1947
        let mut shader_defs = vec![];
×
1948

1949
        let vertex_buffer_layout = key.mesh_layout.as_ref().and_then(|mesh_layout| {
×
1950
            mesh_layout
×
1951
                .0
×
1952
                .get_layout(&[
×
1953
                    Mesh::ATTRIBUTE_POSITION.at_shader_location(0),
×
1954
                    Mesh::ATTRIBUTE_UV_0.at_shader_location(1),
×
1955
                    Mesh::ATTRIBUTE_NORMAL.at_shader_location(2),
×
1956
                ])
1957
                .ok()
×
1958
        });
1959

1960
        if let Some(material_bind_group_layout) = self.get_material(&key.texture_layout) {
×
1961
            layout.push(material_bind_group_layout.clone());
1962
        }
1963

1964
        // Key: LOCAL_SPACE_SIMULATION
1965
        if key.local_space_simulation {
×
1966
            shader_defs.push("LOCAL_SPACE_SIMULATION".into());
×
1967
        }
1968

1969
        match key.alpha_mask {
×
1970
            ParticleRenderAlphaMaskPipelineKey::Blend => {}
×
1971
            ParticleRenderAlphaMaskPipelineKey::AlphaMask => {
1972
                // Key: USE_ALPHA_MASK
1973
                shader_defs.push("USE_ALPHA_MASK".into())
×
1974
            }
1975
            ParticleRenderAlphaMaskPipelineKey::Opaque => {
1976
                // Key: OPAQUE
1977
                shader_defs.push("OPAQUE".into())
×
1978
            }
1979
        }
1980

1981
        // Key: FLIPBOOK
1982
        if key.flipbook {
×
1983
            shader_defs.push("FLIPBOOK".into());
×
1984
        }
1985

1986
        // Key: NEEDS_UV
1987
        if key.needs_uv {
×
1988
            shader_defs.push("NEEDS_UV".into());
×
1989
        }
1990

1991
        // Key: NEEDS_NORMAL
1992
        if key.needs_normal {
×
1993
            shader_defs.push("NEEDS_NORMAL".into());
×
1994
        }
1995

1996
        // Key: RIBBONS
1997
        if key.ribbons {
×
1998
            shader_defs.push("RIBBONS".into());
×
1999
        }
2000

2001
        #[cfg(feature = "2d")]
2002
        let depth_stencil_2d = DepthStencilState {
2003
            format: CORE_2D_DEPTH_FORMAT,
2004
            // Use depth buffer with alpha-masked particles, not with transparent ones
2005
            depth_write_enabled: false, // TODO - opaque/alphamask 2d
2006
            // Bevy uses reverse-Z, so GreaterEqual really means closer
2007
            depth_compare: CompareFunction::GreaterEqual,
2008
            stencil: StencilState::default(),
×
2009
            bias: DepthBiasState::default(),
×
2010
        };
2011

2012
        #[cfg(feature = "3d")]
2013
        let depth_stencil_3d = DepthStencilState {
2014
            format: CORE_3D_DEPTH_FORMAT,
2015
            // Use depth buffer with alpha-masked or opaque particles, not
2016
            // with transparent ones
2017
            depth_write_enabled: matches!(
×
2018
                key.alpha_mask,
2019
                ParticleRenderAlphaMaskPipelineKey::AlphaMask
2020
                    | ParticleRenderAlphaMaskPipelineKey::Opaque
2021
            ),
2022
            // Bevy uses reverse-Z, so GreaterEqual really means closer
2023
            depth_compare: CompareFunction::GreaterEqual,
2024
            stencil: StencilState::default(),
×
2025
            bias: DepthBiasState::default(),
×
2026
        };
2027

2028
        #[cfg(all(feature = "2d", feature = "3d"))]
2029
        assert_eq!(CORE_2D_DEPTH_FORMAT, CORE_3D_DEPTH_FORMAT);
×
2030
        #[cfg(all(feature = "2d", feature = "3d"))]
2031
        let depth_stencil = match key.pipeline_mode {
×
2032
            PipelineMode::Camera2d => Some(depth_stencil_2d),
×
2033
            PipelineMode::Camera3d => Some(depth_stencil_3d),
×
2034
        };
2035

2036
        #[cfg(all(feature = "2d", not(feature = "3d")))]
2037
        let depth_stencil = Some(depth_stencil_2d);
2038

2039
        #[cfg(all(feature = "3d", not(feature = "2d")))]
2040
        let depth_stencil = Some(depth_stencil_3d);
2041

2042
        let format = if key.hdr {
×
2043
            ViewTarget::TEXTURE_FORMAT_HDR
×
2044
        } else {
2045
            TextureFormat::bevy_default()
×
2046
        };
2047

2048
        let hash = calc_func_id(&key);
×
2049
        let label = format!("hanabi:pipeline:render_{hash:016X}");
×
2050
        trace!(
×
2051
            "-> creating pipeline '{}' with shader defs:{}",
×
2052
            label,
×
2053
            shader_defs
×
2054
                .iter()
×
2055
                .fold(String::new(), |acc, x| acc + &format!(" {x:?}"))
×
2056
        );
2057

2058
        RenderPipelineDescriptor {
2059
            label: Some(label.into()),
×
2060
            vertex: VertexState {
×
2061
                shader: key.shader.clone(),
2062
                entry_point: "vertex".into(),
2063
                shader_defs: shader_defs.clone(),
2064
                buffers: vec![vertex_buffer_layout.expect("Vertex buffer layout not present")],
2065
            },
2066
            fragment: Some(FragmentState {
×
2067
                shader: key.shader,
2068
                shader_defs,
2069
                entry_point: "fragment".into(),
2070
                targets: vec![Some(ColorTargetState {
2071
                    format,
2072
                    blend: Some(key.alpha_mode.into()),
2073
                    write_mask: ColorWrites::ALL,
2074
                })],
2075
            }),
2076
            layout,
2077
            primitive: PrimitiveState {
×
2078
                front_face: FrontFace::Ccw,
2079
                cull_mode: None,
2080
                unclipped_depth: false,
2081
                polygon_mode: PolygonMode::Fill,
2082
                conservative: false,
2083
                topology: PrimitiveTopology::TriangleList,
2084
                strip_index_format: None,
2085
            },
2086
            depth_stencil,
2087
            multisample: MultisampleState {
×
2088
                count: key.msaa_samples,
2089
                mask: !0,
2090
                alpha_to_coverage_enabled: false,
2091
            },
2092
            push_constant_ranges: Vec::new(),
×
2093
            zero_initialize_workgroup_memory: false,
2094
        }
2095
    }
2096
}
2097

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

2150
pub struct AddedEffectParent {
2151
    pub entity: MainEntity,
2152
    pub layout: ParticleLayout,
2153
    /// GPU spawn event count to allocate for this effect.
2154
    pub event_count: u32,
2155
}
2156

2157
/// Extracted data for newly-added [`ParticleEffect`] component requiring a new
2158
/// GPU allocation.
2159
///
2160
/// [`ParticleEffect`]: crate::ParticleEffect
2161
pub struct AddedEffect {
2162
    /// Entity with a newly-added [`ParticleEffect`] component.
2163
    ///
2164
    /// [`ParticleEffect`]: crate::ParticleEffect
2165
    pub entity: MainEntity,
2166
    #[allow(dead_code)]
2167
    pub render_entity: RenderEntity,
2168
    /// Capacity, in number of particles, of the effect.
2169
    pub capacity: u32,
2170
    /// Resolved particle mesh, either the one provided by the user or the
2171
    /// default one. This should always be valid.
2172
    pub mesh: Handle<Mesh>,
2173
    /// Parent effect, if any.
2174
    pub parent: Option<AddedEffectParent>,
2175
    /// Layout of particle attributes.
2176
    pub particle_layout: ParticleLayout,
2177
    /// Layout of properties for the effect, if properties are used at all, or
2178
    /// an empty layout.
2179
    pub property_layout: PropertyLayout,
2180
    /// Effect flags.
2181
    pub layout_flags: LayoutFlags,
2182
    /// Handle of the effect asset.
2183
    pub handle: Handle<EffectAsset>,
2184
}
2185

2186
/// Collection of all extracted effects for this frame, inserted into the
2187
/// render world as a render resource.
2188
#[derive(Default, Resource)]
2189
pub(crate) struct ExtractedEffects {
2190
    /// Extracted effects this frame.
2191
    pub effects: Vec<ExtractedEffect>,
2192
    /// Newly added effects without a GPU allocation yet.
2193
    pub added_effects: Vec<AddedEffect>,
2194
}
2195

2196
#[derive(Default, Resource)]
2197
pub(crate) struct EffectAssetEvents {
2198
    pub images: Vec<AssetEvent<Image>>,
2199
}
2200

2201
/// System extracting all the asset events for the [`Image`] assets to enable
2202
/// dynamic update of images bound to any effect.
2203
///
2204
/// This system runs in parallel of [`extract_effects`].
2205
pub(crate) fn extract_effect_events(
×
2206
    mut events: ResMut<EffectAssetEvents>,
2207
    mut image_events: Extract<EventReader<AssetEvent<Image>>>,
2208
) {
2209
    #[cfg(feature = "trace")]
2210
    let _span = bevy::utils::tracing::info_span!("extract_effect_events").entered();
×
2211
    trace!("extract_effect_events()");
×
2212

2213
    let EffectAssetEvents { ref mut images } = *events;
×
2214
    *images = image_events.read().copied().collect();
×
2215
}
2216

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

2251
    /// Enable automatically starting a GPU debugger capture when one or more
2252
    /// effects are spawned.
2253
    ///
2254
    /// Enable this feature to automatically capture one or more GPU frames when
2255
    /// a new effect is spawned (as detected by ECS change detection). This
2256
    /// instructs any attached GPU debugger to start a capture; this has no
2257
    /// effect if no debugger is attached.
2258
    pub start_capture_on_new_effect: bool,
2259

2260
    /// Number of frames to capture with a GPU debugger.
2261
    ///
2262
    /// By default this value is zero, and a GPU debugger capture runs for a
2263
    /// single frame. If a non-zero frame count is specified here, the capture
2264
    /// will instead stop once the specified number of frames has been recorded.
2265
    ///
2266
    /// You should avoid setting this to a value too large, to prevent the
2267
    /// capture size from getting out of control. A typical value is 1 to 3
2268
    /// frames, or possibly more (up to 10) for exceptional contexts. Some GPU
2269
    /// debuggers or graphics APIs might further limit this value on their own,
2270
    /// so there's no guarantee the graphics API will honor this value.
2271
    pub capture_frame_count: u32,
2272
}
2273

2274
#[derive(Debug, Default, Clone, Copy, Resource)]
2275
pub(crate) struct RenderDebugSettings {
2276
    /// Is a GPU debugger capture on-going?
2277
    is_capturing: bool,
2278
    /// Start time of any on-going GPU debugger capture.
2279
    capture_start: Duration,
2280
    /// Number of frames captured so far for on-going GPU debugger capture.
2281
    captured_frames: u32,
2282
}
2283

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

2336
    // Manage GPU debug capture
2337
    if render_debug_settings.is_capturing {
×
2338
        render_debug_settings.captured_frames += 1;
×
2339

2340
        // Stop any pending capture if needed
2341
        if render_debug_settings.captured_frames >= debug_settings.capture_frame_count {
×
2342
            render_device.wgpu_device().stop_capture();
×
2343
            render_debug_settings.is_capturing = false;
×
2344
            warn!(
×
2345
                "Stopped GPU debug capture after {} frames, at t={}s.",
×
2346
                render_debug_settings.captured_frames,
×
2347
                real_time.elapsed().as_secs_f64()
×
2348
            );
2349
        }
2350
    }
2351
    if !render_debug_settings.is_capturing {
×
2352
        // If no pending capture, consider starting a new one
2353
        if debug_settings.start_capture_this_frame
×
2354
            || (debug_settings.start_capture_on_new_effect && !q_added_effects.is_empty())
×
2355
        {
2356
            render_device.wgpu_device().start_capture();
×
2357
            render_debug_settings.is_capturing = true;
×
2358
            render_debug_settings.capture_start = real_time.elapsed();
×
2359
            render_debug_settings.captured_frames = 0;
×
2360
            warn!(
×
2361
                "Started GPU debug capture at t={}s.",
×
2362
                render_debug_settings.capture_start.as_secs_f64()
×
2363
            );
2364
        }
2365
    }
2366

2367
    // Save simulation params into render world
2368
    sim_params.time = time.elapsed_secs_f64();
×
2369
    sim_params.delta_time = time.delta_secs();
×
2370
    sim_params.virtual_time = virtual_time.elapsed_secs_f64();
×
2371
    sim_params.virtual_delta_time = virtual_time.delta_secs();
×
2372
    sim_params.real_time = real_time.elapsed_secs_f64();
×
2373
    sim_params.real_delta_time = real_time.delta_secs();
×
2374

2375
    // Collect added effects for later GPU data allocation
2376
    extracted_effects.added_effects = q_added_effects
×
2377
        .iter()
×
2378
        .chain(mem::take(&mut *pending_effects).into_iter().filter_map(|main_entity| {
×
2379
            q_all_effects.get(main_entity.id()).ok().map(|(render_entity, compiled_particle_effect)| {
×
2380
                (main_entity.id(), render_entity, compiled_particle_effect)
×
2381
            })
2382
        }))
2383
        .filter_map(|(entity, render_entity, compiled_effect)| {
×
2384
            let handle = compiled_effect.asset.clone_weak();
×
2385
            let asset = match effects.get(&compiled_effect.asset) {
×
2386
                None => {
2387
                    // The effect wasn't ready yet. Retry on subsequent frames.
2388
                    trace!("Failed to find asset for {:?}/{:?}, deferring to next frame", entity, render_entity);
×
2389
                    pending_effects.push(entity.into());
2390
                    return None;
2391
                }
2392
                Some(asset) => asset,
×
2393
            };
2394
            let particle_layout = asset.particle_layout();
×
2395
            assert!(
×
2396
                particle_layout.size() > 0,
×
2397
                "Invalid empty particle layout for effect '{}' on entity {:?} (render entity {:?}). Did you forget to add some modifier to the asset?",
×
2398
                asset.name,
×
2399
                entity,
×
2400
                render_entity.id(),
×
2401
            );
2402
            let property_layout = asset.property_layout();
×
2403
            let mesh = compiled_effect
×
2404
                .mesh
×
2405
                .clone()
×
2406
                .unwrap_or(default_mesh.0.clone());
×
2407

2408
            trace!(
×
2409
                "Found new effect: entity {:?} | render entity {:?} | capacity {:?} | particle_layout {:?} | \
×
2410
                 property_layout {:?} | layout_flags {:?} | mesh {:?}",
×
2411
                 entity,
×
2412
                 render_entity.id(),
×
2413
                 asset.capacity(),
×
2414
                 particle_layout,
2415
                 property_layout,
2416
                 compiled_effect.layout_flags,
2417
                 mesh);
2418

2419
            // 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
2420
            const FIXME_HARD_CODED_EVENT_COUNT: u32 = 256;
2421
            let parent = compiled_effect.parent.map(|entity| AddedEffectParent {
×
2422
                entity: entity.into(),
×
2423
                layout: compiled_effect.parent_particle_layout.as_ref().unwrap().clone(),
×
2424
                event_count: FIXME_HARD_CODED_EVENT_COUNT,
×
2425
            });
2426

2427
            trace!("Found new effect: entity {:?} | capacity {:?} | particle_layout {:?} | property_layout {:?} | layout_flags {:?}", entity, asset.capacity(), particle_layout, property_layout, compiled_effect.layout_flags);
×
2428
            Some(AddedEffect {
×
2429
                entity: MainEntity::from(entity),
×
2430
                render_entity: *render_entity,
×
2431
                capacity: asset.capacity(),
×
2432
                mesh,
×
2433
                parent,
×
2434
                particle_layout,
×
2435
                property_layout,
×
2436
                layout_flags: compiled_effect.layout_flags,
×
2437
                handle,
×
2438
            })
2439
        })
2440
        .collect();
×
2441

2442
    // Loop over all existing effects to extract them
2443
    extracted_effects.effects.clear();
×
2444
    for (
2445
        main_entity,
×
2446
        render_entity,
×
2447
        maybe_inherited_visibility,
×
2448
        maybe_view_visibility,
×
2449
        effect_spawner,
×
2450
        compiled_effect,
×
2451
        maybe_properties,
×
2452
        transform,
×
2453
    ) in q_effects.iter()
×
2454
    {
2455
        // Check if shaders are configured
2456
        let Some(effect_shaders) = compiled_effect.get_configured_shaders() else {
×
2457
            continue;
×
2458
        };
2459

2460
        // Check if hidden, unless always simulated
2461
        if compiled_effect.simulation_condition == SimulationCondition::WhenVisible
2462
            && !maybe_inherited_visibility
×
2463
                .map(|cv| cv.get())
×
2464
                .unwrap_or(true)
×
2465
            && !maybe_view_visibility.map(|cv| cv.get()).unwrap_or(true)
×
2466
        {
2467
            continue;
×
2468
        }
2469

2470
        // Check if asset is available, otherwise silently ignore
2471
        let Some(asset) = effects.get(&compiled_effect.asset) else {
×
2472
            trace!(
×
2473
                "EffectAsset not ready; skipping ParticleEffect instance on entity {:?}.",
×
2474
                main_entity
2475
            );
2476
            continue;
×
2477
        };
2478

2479
        // Resolve the render entity of the parent, if any
2480
        let _parent = if let Some(main_entity) = compiled_effect.parent {
×
2481
            let Ok((_, render_entity, _, _, _, _, _, _)) = q_effects.get(main_entity) else {
×
2482
                error!(
×
2483
                    "Failed to resolve render entity of parent with main entity {:?}.",
×
2484
                    main_entity
2485
                );
2486
                continue;
×
2487
            };
2488
            Some(*render_entity)
2489
        } else {
2490
            None
×
2491
        };
2492

2493
        let property_layout = asset.property_layout();
2494
        let property_data = if let Some(properties) = maybe_properties {
×
2495
            // Note: must check that property layout is not empty, because the
2496
            // EffectProperties component is marked as changed when added but contains an
2497
            // empty Vec if there's no property, which would later raise an error if we
2498
            // don't return None here.
2499
            if properties.is_changed() && !property_layout.is_empty() {
×
2500
                trace!("Detected property change, re-serializing...");
×
2501
                Some(properties.serialize(&property_layout))
2502
            } else {
2503
                None
×
2504
            }
2505
        } else {
2506
            None
×
2507
        };
2508

2509
        let texture_layout = asset.module().texture_layout();
2510
        let layout_flags = compiled_effect.layout_flags;
2511
        // let mesh = compiled_effect
2512
        //     .mesh
2513
        //     .clone()
2514
        //     .unwrap_or(default_mesh.0.clone());
2515
        let alpha_mode = compiled_effect.alpha_mode;
2516

2517
        trace!(
2518
            "Extracted instance of effect '{}' on entity {:?} (render entity {:?}): texture_layout_count={} texture_count={} layout_flags={:?}",
×
2519
            asset.name,
×
2520
            main_entity,
×
2521
            render_entity.id(),
×
2522
            texture_layout.layout.len(),
×
2523
            compiled_effect.textures.len(),
×
2524
            layout_flags,
2525
        );
2526

2527
        extracted_effects.effects.push(ExtractedEffect {
2528
            render_entity: *render_entity,
2529
            main_entity: main_entity.into(),
2530
            handle: compiled_effect.asset.clone_weak(),
2531
            particle_layout: asset.particle_layout().clone(),
2532
            property_layout,
2533
            property_data,
2534
            spawn_count: effect_spawner.spawn_count,
2535
            prng_seed: compiled_effect.prng_seed,
2536
            transform: *transform,
2537
            layout_flags,
2538
            texture_layout,
2539
            textures: compiled_effect.textures.clone(),
2540
            alpha_mode,
2541
            effect_shaders: effect_shaders.clone(),
2542
        });
2543
    }
2544
}
2545

2546
/// Various GPU limits and aligned sizes computed once and cached.
2547
struct GpuLimits {
2548
    /// Value of [`WgpuLimits::min_storage_buffer_offset_alignment`].
2549
    ///
2550
    /// [`WgpuLimits::min_storage_buffer_offset_alignment`]: bevy::render::settings::WgpuLimits::min_storage_buffer_offset_alignment
2551
    storage_buffer_align: NonZeroU32,
2552

2553
    /// Size of [`GpuEffectMetadata`] aligned to the contraint of
2554
    /// [`WgpuLimits::min_storage_buffer_offset_alignment`].
2555
    ///
2556
    /// [`WgpuLimits::min_storage_buffer_offset_alignment`]: bevy::render::settings::WgpuLimits::min_storage_buffer_offset_alignment
2557
    effect_metadata_aligned_size: NonZeroU32,
2558
}
2559

2560
impl GpuLimits {
2561
    pub fn from_device(render_device: &RenderDevice) -> Self {
1✔
2562
        let storage_buffer_align =
1✔
2563
            render_device.limits().min_storage_buffer_offset_alignment as u64;
1✔
2564

2565
        let effect_metadata_aligned_size = NonZeroU32::new(
2566
            GpuEffectMetadata::min_size()
1✔
2567
                .get()
1✔
2568
                .next_multiple_of(storage_buffer_align) as u32,
1✔
2569
        )
2570
        .unwrap();
2571

2572
        trace!(
1✔
2573
            "GPU-aligned sizes (align: {} B):\n- GpuEffectMetadata: {} B -> {} B",
×
2574
            storage_buffer_align,
×
2575
            GpuEffectMetadata::min_size().get(),
×
2576
            effect_metadata_aligned_size.get(),
×
2577
        );
2578

2579
        Self {
2580
            storage_buffer_align: NonZeroU32::new(storage_buffer_align as u32).unwrap(),
1✔
2581
            effect_metadata_aligned_size,
2582
        }
2583
    }
2584

2585
    /// Byte alignment for any storage buffer binding.
2586
    pub fn storage_buffer_align(&self) -> NonZeroU32 {
×
2587
        self.storage_buffer_align
×
2588
    }
2589

2590
    /// Byte offset of the [`GpuEffectMetadata`] of a given buffer.
2591
    pub fn effect_metadata_offset(&self, buffer_index: u32) -> u64 {
1✔
2592
        self.effect_metadata_aligned_size.get() as u64 * buffer_index as u64
1✔
2593
    }
2594

2595
    /// Byte alignment for [`GpuEffectMetadata`].
2596
    pub fn effect_metadata_size(&self) -> NonZeroU64 {
×
2597
        NonZeroU64::new(self.effect_metadata_aligned_size.get() as u64).unwrap()
×
2598
    }
2599
}
2600

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

2647
impl EffectsMeta {
2648
    pub fn new(
×
2649
        device: RenderDevice,
2650
        indirect_shader_noevent: Handle<Shader>,
2651
        indirect_shader_events: Handle<Shader>,
2652
    ) -> Self {
2653
        let gpu_limits = GpuLimits::from_device(&device);
×
2654

2655
        // Ensure individual GpuSpawnerParams elements are properly aligned so they can
2656
        // be addressed individually by the computer shaders.
2657
        let item_align = gpu_limits.storage_buffer_align().get() as u64;
×
2658
        trace!(
×
2659
            "Aligning storage buffers to {} bytes as device limits requires.",
×
2660
            item_align
2661
        );
2662

2663
        Self {
2664
            view_bind_group: None,
2665
            indirect_sim_params_bind_group: None,
2666
            indirect_metadata_bind_group: None,
2667
            indirect_spawner_bind_group: None,
2668
            sim_params_uniforms: UniformBuffer::default(),
×
2669
            spawner_buffer: AlignedBufferVec::new(
×
2670
                BufferUsages::STORAGE,
2671
                NonZeroU64::new(item_align),
2672
                Some("hanabi:buffer:spawner".to_string()),
2673
            ),
NEW
2674
            update_dispatch_indirect_buffer: GpuBuffer::new(
×
2675
                BufferUsages::STORAGE | BufferUsages::INDIRECT,
2676
                Some("hanabi:buffer:update_dispatch_indirect".to_string()),
2677
            ),
2678
            effect_metadata_buffer: BufferTable::new(
×
2679
                BufferUsages::STORAGE | BufferUsages::INDIRECT,
2680
                NonZeroU64::new(item_align),
2681
                Some("hanabi:buffer:effect_metadata".to_string()),
2682
            ),
2683
            gpu_limits,
2684
            indirect_shader_noevent,
2685
            indirect_shader_events,
2686
            indirect_pipeline_ids: [
×
2687
                CachedComputePipelineId::INVALID,
2688
                CachedComputePipelineId::INVALID,
2689
            ],
2690
            active_indirect_pipeline_id: CachedComputePipelineId::INVALID,
2691
        }
2692
    }
2693

2694
    /// Allocate internal resources for newly spawned effects.
2695
    ///
2696
    /// After this system ran, all valid extracted effects from the main world
2697
    /// have a corresponding entity with a [`CachedEffect`] component in the
2698
    /// render world. An extracted effect is considered valid if it passed some
2699
    /// basic checks, like having a valid mesh. Note however that the main
2700
    /// world's entity might still be missing its [`RenderEntity`]
2701
    /// reference, since we cannot yet write into the main world.
2702
    pub fn add_effects(
×
2703
        &mut self,
2704
        mut commands: Commands,
2705
        mut added_effects: Vec<AddedEffect>,
2706
        mesh_allocator: &MeshAllocator,
2707
        render_meshes: &RenderAssets<RenderMesh>,
2708
        effect_cache: &mut ResMut<EffectCache>,
2709
        property_cache: &mut ResMut<PropertyCache>,
2710
        event_cache: &mut ResMut<EventCache>,
2711
    ) {
2712
        // FIXME - We delete a buffer above, and have a chance to immediatly re-create
2713
        // it below. We should keep the GPU buffer around until the end of this method.
2714
        // On the other hand, we should also be careful that allocated buffers need to
2715
        // be tightly packed because 'vfx_indirect.wgsl' index them by buffer index in
2716
        // order, so doesn't support offset.
2717

2718
        trace!("Adding {} newly spawned effects", added_effects.len());
×
2719
        for added_effect in added_effects.drain(..) {
×
2720
            trace!("+ added effect: capacity={}", added_effect.capacity);
×
2721

2722
            // Allocate an indirect dispatch arguments struct for this instance
2723
            let update_dispatch_indirect_buffer_row_index =
2724
                self.update_dispatch_indirect_buffer.allocate();
2725

2726
            // Allocate per-effect metadata. Note that we run after Bevy has allocated
2727
            // meshes, so we already know the buffer and position of the particle mesh, and
2728
            // can fill the indirect args with it.
2729
            let (gpu_effect_metadata, cached_mesh) = {
×
2730
                // FIXME - this is too soon because prepare_assets::<RenderMesh>() didn't
2731
                // necessarily run. we should defer CachedMesh until later,
2732
                // as we don't really need it here anyway. use Added<CachedEffect> to detect
2733
                // newly added effects later in the render frame? note also that
2734
                // we use cmd.get(entity).insert() so technically the CachedEffect _could_
2735
                // already exist... maybe should only do the bare minimum here
2736
                // (insert into caches) and not update components eagerly? not sure...
2737

2738
                let Some(render_mesh) = render_meshes.get(added_effect.mesh.id()) else {
×
2739
                    warn!(
×
2740
                        "Cannot find render mesh of particle effect instance on entity {:?}, despite applying default mesh. Invalid asset handle: {:?}",
×
2741
                        added_effect.entity, added_effect.mesh
2742
                    );
2743
                    continue;
×
2744
                };
2745
                let Some(mesh_vertex_buffer_slice) =
×
2746
                    mesh_allocator.mesh_vertex_slice(&added_effect.mesh.id())
2747
                else {
2748
                    trace!(
×
2749
                        "Effect main_entity {:?}: cannot find vertex slice of render mesh {:?}",
×
2750
                        added_effect.entity,
2751
                        added_effect.mesh
2752
                    );
2753
                    continue;
×
2754
                };
2755
                let mesh_index_buffer_slice =
2756
                    mesh_allocator.mesh_index_slice(&added_effect.mesh.id());
2757
                let indexed = if let RenderMeshBufferInfo::Indexed { index_format, .. } =
×
2758
                    render_mesh.buffer_info
2759
                {
2760
                    if let Some(ref slice) = mesh_index_buffer_slice {
×
2761
                        Some(MeshIndexSlice {
2762
                            format: index_format,
2763
                            buffer: slice.buffer.clone(),
2764
                            range: slice.range.clone(),
2765
                        })
2766
                    } else {
2767
                        trace!(
×
2768
                            "Effect main_entity {:?}: cannot find index slice of render mesh {:?}",
×
2769
                            added_effect.entity,
2770
                            added_effect.mesh
2771
                        );
2772
                        continue;
×
2773
                    }
2774
                } else {
2775
                    None
×
2776
                };
2777

2778
                (
2779
                    match &mesh_index_buffer_slice {
2780
                        // Indexed mesh rendering
2781
                        Some(mesh_index_buffer_slice) => {
×
2782
                            let ret = GpuEffectMetadata {
×
2783
                                vertex_or_index_count: mesh_index_buffer_slice.range.len() as u32,
×
2784
                                instance_count: 0,
×
2785
                                first_index_or_vertex_offset: mesh_index_buffer_slice.range.start,
×
2786
                                vertex_offset_or_base_instance: mesh_vertex_buffer_slice.range.start
×
2787
                                    as i32,
×
2788
                                base_instance: 0,
×
2789
                                alive_count: 0,
×
2790
                                max_update: 0,
×
2791
                                dead_count: added_effect.capacity,
×
2792
                                max_spawn: added_effect.capacity,
×
2793
                                ..default()
×
2794
                            };
2795
                            trace!("+ Effect[indexed]: {:?}", ret);
×
2796
                            ret
×
2797
                        }
2798
                        // Non-indexed mesh rendering
2799
                        None => {
2800
                            let ret = GpuEffectMetadata {
×
2801
                                vertex_or_index_count: mesh_vertex_buffer_slice.range.len() as u32,
×
2802
                                instance_count: 0,
×
2803
                                first_index_or_vertex_offset: mesh_vertex_buffer_slice.range.start,
×
2804
                                vertex_offset_or_base_instance: 0,
×
2805
                                base_instance: 0,
×
2806
                                alive_count: 0,
×
2807
                                max_update: 0,
×
2808
                                dead_count: added_effect.capacity,
×
2809
                                max_spawn: added_effect.capacity,
×
2810
                                ..default()
×
2811
                            };
2812
                            trace!("+ Effect[non-indexed]: {:?}", ret);
×
2813
                            ret
2814
                        }
2815
                    },
2816
                    CachedMesh {
2817
                        mesh: added_effect.mesh.id(),
2818
                        buffer: mesh_vertex_buffer_slice.buffer.clone(),
2819
                        range: mesh_vertex_buffer_slice.range.clone(),
2820
                        indexed,
2821
                    },
2822
                )
2823
            };
2824
            let effect_metadata_buffer_table_id =
2825
                self.effect_metadata_buffer.insert(gpu_effect_metadata);
2826
            let dispatch_buffer_indices = DispatchBufferIndices {
2827
                update_dispatch_indirect_buffer_row_index,
2828
                effect_metadata_buffer_table_id,
2829
            };
2830

2831
            // Insert the effect into the cache. This will allocate all the necessary
2832
            // mandatory GPU resources as needed.
2833
            let cached_effect = effect_cache.insert(
2834
                added_effect.handle,
2835
                added_effect.capacity,
2836
                &added_effect.particle_layout,
2837
                added_effect.layout_flags,
2838
            );
2839
            let mut cmd = commands.entity(added_effect.render_entity.id());
2840
            cmd.insert((
2841
                added_effect.entity,
2842
                cached_effect,
2843
                dispatch_buffer_indices,
2844
                cached_mesh,
2845
            ));
2846

2847
            // Allocate storage for properties if needed
2848
            if !added_effect.property_layout.is_empty() {
×
2849
                let cached_effect_properties = property_cache.insert(&added_effect.property_layout);
×
2850
                cmd.insert(cached_effect_properties);
×
2851
            } else {
2852
                cmd.remove::<CachedEffectProperties>();
×
2853
            }
2854

2855
            // Allocate storage for the reference to the parent effect if needed. Note that
2856
            // we cannot yet allocate the complete parent info (CachedChildInfo) because it
2857
            // depends on the list of children, which we can't resolve until all
2858
            // effects have been added/removed this frame. This will be done later in
2859
            // resolve_parents().
2860
            if let Some(parent) = added_effect.parent.as_ref() {
×
2861
                let cached_parent: CachedParentRef = CachedParentRef {
2862
                    entity: parent.entity,
2863
                };
2864
                cmd.insert(cached_parent);
2865
                trace!("+ new effect declares parent entity {:?}", parent.entity);
×
2866
            } else {
2867
                cmd.remove::<CachedParentRef>();
×
2868
                trace!("+ new effect declares no parent");
×
2869
            }
2870

2871
            // Allocate storage for GPU spawn events if needed
2872
            if let Some(parent) = added_effect.parent.as_ref() {
×
2873
                let cached_events = event_cache.allocate(parent.event_count);
2874
                cmd.insert(cached_events);
2875
            } else {
2876
                cmd.remove::<CachedEffectEvents>();
×
2877
            }
2878

2879
            // Ensure the particle@1 bind group layout exists for the given configuration of
2880
            // particle layout and (optionally) parent particle layout.
2881
            {
2882
                let parent_min_binding_size = added_effect
2883
                    .parent
2884
                    .map(|added_parent| added_parent.layout.min_binding_size32());
×
2885
                effect_cache.ensure_particle_bind_group_layout(
2886
                    added_effect.particle_layout.min_binding_size32(),
2887
                    parent_min_binding_size,
2888
                );
2889
            }
2890

2891
            // Ensure the metadata@3 bind group layout exists for init pass.
2892
            {
2893
                let consume_gpu_spawn_events = added_effect
2894
                    .layout_flags
2895
                    .contains(LayoutFlags::CONSUME_GPU_SPAWN_EVENTS);
2896
                effect_cache.ensure_metadata_init_bind_group_layout(consume_gpu_spawn_events);
2897
            }
2898

2899
            // We cannot yet determine the layout of the metadata@3 bind group for the
2900
            // update pass, because it depends on the number of children, and
2901
            // this is encoded indirectly via the number of child effects
2902
            // pointing to this parent, and only calculated later in
2903
            // resolve_parents().
2904

2905
            trace!(
2906
                "+ added effect entity {:?}: main_entity={:?} \
×
2907
                first_update_group_dispatch_buffer_index={} \
×
2908
                render_effect_dispatch_buffer_id={}",
×
2909
                added_effect.render_entity,
2910
                added_effect.entity,
2911
                update_dispatch_indirect_buffer_row_index,
2912
                effect_metadata_buffer_table_id.0
2913
            );
2914
        }
2915
    }
2916

2917
    pub fn allocate_spawner(
×
2918
        &mut self,
2919
        global_transform: &GlobalTransform,
2920
        spawn_count: u32,
2921
        prng_seed: u32,
2922
        effect_metadata_buffer_table_id: BufferTableId,
2923
    ) -> u32 {
2924
        let spawner_base = self.spawner_buffer.len() as u32;
×
2925
        let transform = global_transform.compute_matrix().into();
×
2926
        let inverse_transform = Mat4::from(
2927
            // Inverse the Affine3A first, then convert to Mat4. This is a lot more
2928
            // efficient than inversing the Mat4.
2929
            global_transform.affine().inverse(),
×
2930
        )
2931
        .into();
2932
        let spawner_params = GpuSpawnerParams {
2933
            transform,
2934
            inverse_transform,
2935
            spawn: spawn_count as i32,
×
2936
            seed: prng_seed,
2937
            effect_metadata_index: effect_metadata_buffer_table_id.0,
×
2938
            ..default()
2939
        };
2940
        trace!("spawner params = {:?}", spawner_params);
×
2941
        self.spawner_buffer.push(spawner_params);
×
2942
        spawner_base
×
2943
    }
2944
}
2945

2946
bitflags! {
2947
    /// Effect flags.
2948
    #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
2949
    pub struct LayoutFlags: u32 {
2950
        /// No flags.
2951
        const NONE = 0;
2952
        // DEPRECATED - The effect uses an image texture.
2953
        //const PARTICLE_TEXTURE = (1 << 0);
2954
        /// The effect is simulated in local space.
2955
        const LOCAL_SPACE_SIMULATION = (1 << 2);
2956
        /// The effect uses alpha masking instead of alpha blending. Only used for 3D.
2957
        const USE_ALPHA_MASK = (1 << 3);
2958
        /// The effect is rendered with flipbook texture animation based on the
2959
        /// [`Attribute::SPRITE_INDEX`] of each particle.
2960
        const FLIPBOOK = (1 << 4);
2961
        /// The effect needs UVs.
2962
        const NEEDS_UV = (1 << 5);
2963
        /// The effect has ribbons.
2964
        const RIBBONS = (1 << 6);
2965
        /// The effects needs normals.
2966
        const NEEDS_NORMAL = (1 << 7);
2967
        /// The effect is fully-opaque.
2968
        const OPAQUE = (1 << 8);
2969
        /// The (update) shader emits GPU spawn events to instruct another effect to spawn particles.
2970
        const EMIT_GPU_SPAWN_EVENTS = (1 << 9);
2971
        /// The (init) shader spawns particles by consuming GPU spawn events, instead of
2972
        /// a single CPU spawn count.
2973
        const CONSUME_GPU_SPAWN_EVENTS = (1 << 10);
2974
        /// The (init or update) shader needs access to its parent particle. This allows
2975
        /// a particle init or update pass to read the data of a parent particle, for
2976
        /// example to inherit some of the attributes.
2977
        const READ_PARENT_PARTICLE = (1 << 11);
2978
    }
2979
}
2980

2981
impl Default for LayoutFlags {
2982
    fn default() -> Self {
1✔
2983
        Self::NONE
1✔
2984
    }
2985
}
2986

2987
/// Observer raised when the [`CachedEffect`] component is removed, which
2988
/// indicates that the effect instance was despawned.
2989
pub(crate) fn on_remove_cached_effect(
×
2990
    trigger: Trigger<OnRemove, CachedEffect>,
2991
    query: Query<(
2992
        Entity,
2993
        MainEntity,
2994
        &CachedEffect,
2995
        &DispatchBufferIndices,
2996
        Option<&CachedEffectProperties>,
2997
        Option<&CachedParentInfo>,
2998
        Option<&CachedEffectEvents>,
2999
    )>,
3000
    mut effect_cache: ResMut<EffectCache>,
3001
    mut effect_bind_groups: ResMut<EffectBindGroups>,
3002
    mut effects_meta: ResMut<EffectsMeta>,
3003
    mut event_cache: ResMut<EventCache>,
3004
) {
3005
    #[cfg(feature = "trace")]
3006
    let _span = bevy::utils::tracing::info_span!("on_remove_cached_effect").entered();
×
3007

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

3011
    // Fecth the components of the effect being destroyed. Note that the despawn
3012
    // command above is not yet applied, so this query should always succeed.
3013
    let Ok((
3014
        render_entity,
×
3015
        main_entity,
×
3016
        cached_effect,
×
3017
        dispatch_buffer_indices,
×
3018
        _opt_props,
×
3019
        _opt_parent,
×
3020
        opt_cached_effect_events,
×
3021
    )) = query.get(trigger.entity())
×
3022
    else {
3023
        return;
×
3024
    };
3025

3026
    // Dealllocate the effect slice in the event buffer, if any.
3027
    if let Some(cached_effect_events) = opt_cached_effect_events {
×
3028
        match event_cache.free(cached_effect_events) {
3029
            Err(err) => {
×
3030
                error!("Error while freeing effect event slice: {err:?}");
×
3031
            }
3032
            Ok(buffer_state) => {
×
3033
                if buffer_state != BufferState::Used {
×
3034
                    // Clear bind groups associated with the old buffer
3035
                    effect_bind_groups.init_metadata_bind_groups.clear();
×
3036
                    effect_bind_groups.update_metadata_bind_groups.clear();
×
3037
                }
3038
            }
3039
        }
3040
    }
3041

3042
    // Deallocate the effect slice in the GPU effect buffer, and if this was the
3043
    // last slice, also deallocate the GPU buffer itself.
3044
    trace!(
3045
        "=> ParticleEffect on render entity {:?} associated with main entity {:?}, removing...",
×
3046
        render_entity,
3047
        main_entity,
3048
    );
3049
    let Ok(BufferState::Free) = effect_cache.remove(cached_effect) else {
3050
        // Buffer was not affected, so all bind groups are still valid. Nothing else to
3051
        // do.
3052
        return;
×
3053
    };
3054

3055
    // Clear bind groups associated with the removed buffer
3056
    trace!(
×
3057
        "=> GPU buffer #{} gone, destroying its bind groups...",
×
3058
        cached_effect.buffer_index
3059
    );
3060
    effect_bind_groups
3061
        .particle_buffers
3062
        .remove(&cached_effect.buffer_index);
3063
    effects_meta
3064
        .update_dispatch_indirect_buffer
3065
        .free(dispatch_buffer_indices.update_dispatch_indirect_buffer_row_index);
3066
    effects_meta
3067
        .effect_metadata_buffer
3068
        .remove(dispatch_buffer_indices.effect_metadata_buffer_table_id);
3069
}
3070

3071
/// Update the [`CachedEffect`] component for any newly allocated effect.
3072
///
3073
/// After this system ran, and its commands are applied, all valid extracted
3074
/// effects have a corresponding entity in the render world, with a
3075
/// [`CachedEffect`] component. From there, we operate on those exclusively.
3076
pub(crate) fn add_effects(
×
3077
    mesh_allocator: Res<MeshAllocator>,
3078
    render_meshes: Res<RenderAssets<RenderMesh>>,
3079
    commands: Commands,
3080
    mut effects_meta: ResMut<EffectsMeta>,
3081
    mut effect_cache: ResMut<EffectCache>,
3082
    mut property_cache: ResMut<PropertyCache>,
3083
    mut event_cache: ResMut<EventCache>,
3084
    mut extracted_effects: ResMut<ExtractedEffects>,
3085
    mut sort_bind_groups: ResMut<SortBindGroups>,
3086
) {
3087
    #[cfg(feature = "trace")]
3088
    let _span = bevy::utils::tracing::info_span!("add_effects").entered();
×
3089
    trace!("add_effects");
×
3090

3091
    // Clear last frame's buffer resizes which may have occured during last frame,
3092
    // during `Node::run()` while the `BufferTable` could not be mutated. This is
3093
    // the first point at which we can do that where we're not blocking the main
3094
    // world (so, excluding the extract system).
3095
    effects_meta
×
3096
        .update_dispatch_indirect_buffer
×
3097
        .clear_previous_frame_resizes();
3098
    effects_meta
×
3099
        .effect_metadata_buffer
×
3100
        .clear_previous_frame_resizes();
3101
    sort_bind_groups.clear_previous_frame_resizes();
×
NEW
3102
    event_cache.clear_previous_frame_resizes();
×
3103

3104
    // Allocate new effects
3105
    effects_meta.add_effects(
×
3106
        commands,
×
3107
        std::mem::take(&mut extracted_effects.added_effects),
×
3108
        &mesh_allocator,
×
3109
        &render_meshes,
×
3110
        &mut effect_cache,
×
3111
        &mut property_cache,
×
3112
        &mut event_cache,
×
3113
    );
3114

3115
    // Note: we don't need to explicitly allocate GPU buffers for effects,
3116
    // because EffectBuffer already contains a reference to the
3117
    // RenderDevice, so has done so internally. This is not ideal
3118
    // design-wise, but works.
3119
}
3120

3121
/// Check if two lists of entities are equal.
3122
fn is_child_list_changed(
×
3123
    parent_entity: Entity,
3124
    old: impl ExactSizeIterator<Item = Entity>,
3125
    new: impl ExactSizeIterator<Item = Entity>,
3126
) -> bool {
3127
    if old.len() != new.len() {
×
3128
        trace!(
×
3129
            "Child list changed for effect {:?}: old #{} != new #{}",
×
3130
            parent_entity,
×
3131
            old.len(),
×
3132
            new.len()
×
3133
        );
3134
        return true;
×
3135
    }
3136

3137
    // TODO - this value is arbitrary
3138
    if old.len() >= 16 {
×
3139
        // For large-ish lists, use a hash set.
3140
        let old = HashSet::from_iter(old);
×
3141
        let new = HashSet::from_iter(new);
×
3142
        if old != new {
×
3143
            trace!(
×
3144
                "Child list changed for effect {parent_entity:?}: old [{old:?}] != new [{new:?}]"
×
3145
            );
3146
            true
×
3147
        } else {
3148
            false
×
3149
        }
3150
    } else {
3151
        // For small lists, just use a linear array and sort it
3152
        let mut old = old.collect::<Vec<_>>();
×
3153
        let mut new = new.collect::<Vec<_>>();
×
3154
        old.sort_unstable();
×
3155
        new.sort_unstable();
×
3156
        if old != new {
×
3157
            trace!(
×
3158
                "Child list changed for effect {parent_entity:?}: old [{old:?}] != new [{new:?}]"
×
3159
            );
3160
            true
×
3161
        } else {
3162
            false
×
3163
        }
3164
    }
3165
}
3166

3167
/// Resolve parents and children, updating their [`CachedParent`] and
3168
/// [`CachedChild`] components, as well as (re-)allocating any [`GpuChildInfo`]
3169
/// slice for all children of each parent.
3170
pub(crate) fn resolve_parents(
×
3171
    mut commands: Commands,
3172
    q_child_effects: Query<
3173
        (
3174
            Entity,
3175
            &CachedParentRef,
3176
            &CachedEffectEvents,
3177
            Option<&CachedChildInfo>,
3178
        ),
3179
        With<CachedEffect>,
3180
    >,
3181
    q_cached_effects: Query<(Entity, MainEntity, &CachedEffect)>,
3182
    effect_cache: Res<EffectCache>,
3183
    mut q_parent_effects: Query<(Entity, &mut CachedParentInfo), With<CachedEffect>>,
3184
    mut event_cache: ResMut<EventCache>,
3185
    mut children_from_parent: Local<
3186
        HashMap<Entity, (Vec<(Entity, BufferBindingSource)>, Vec<GpuChildInfo>)>,
3187
    >,
3188
) {
3189
    #[cfg(feature = "trace")]
3190
    let _span = bevy::utils::tracing::info_span!("resolve_parents").entered();
×
3191
    let num_parent_effects = q_parent_effects.iter().len();
×
3192
    trace!("resolve_parents: num_parents={num_parent_effects}");
×
3193

3194
    // Build map of render entity from main entity for all cached effects.
3195
    let render_from_main_entity = q_cached_effects
×
3196
        .iter()
3197
        .map(|(render_entity, main_entity, _)| (main_entity, render_entity))
×
3198
        .collect::<HashMap<_, _>>();
3199

3200
    // Record all parents with children that changed so that we can mark those
3201
    // parents' `CachedParentInfo` as changed. See the comment in the
3202
    // `q_parent_effects` loop for more information.
3203
    let mut parents_with_dirty_children = EntityHashSet::default();
×
3204

3205
    // Group child effects by parent, building a list of children for each parent,
3206
    // solely based on the declaration each child makes of its parent. This doesn't
3207
    // mean yet that the parent exists.
3208
    if children_from_parent.capacity() < num_parent_effects {
×
3209
        let extra = num_parent_effects - children_from_parent.capacity();
×
3210
        children_from_parent.reserve(extra);
×
3211
    }
3212
    for (child_entity, cached_parent_ref, cached_effect_events, cached_child_info) in
×
3213
        q_child_effects.iter()
×
3214
    {
3215
        // Resolve the parent reference into the render world
3216
        let parent_main_entity = cached_parent_ref.entity;
3217
        let Some(parent_entity) = render_from_main_entity.get(&parent_main_entity.id()) else {
×
3218
            warn!(
×
3219
                "Cannot resolve parent render entity for parent main entity {:?}, removing CachedChildInfo from child entity {:?}.",
×
3220
                parent_main_entity, child_entity
3221
            );
3222
            commands.entity(child_entity).remove::<CachedChildInfo>();
×
3223
            continue;
×
3224
        };
3225
        let parent_entity = *parent_entity;
3226

3227
        // Resolve the parent
3228
        let Ok((_, _, parent_cached_effect)) = q_cached_effects.get(parent_entity) else {
×
3229
            // Since we failed to resolve, remove this component so the next systems ignore
3230
            // this effect.
3231
            warn!(
×
3232
                "Unknown parent render entity {:?}, removing CachedChildInfo from child entity {:?}.",
×
3233
                parent_entity, child_entity
3234
            );
3235
            commands.entity(child_entity).remove::<CachedChildInfo>();
×
3236
            continue;
×
3237
        };
3238
        let Some(parent_buffer_binding_source) = effect_cache
×
3239
            .get_buffer(parent_cached_effect.buffer_index)
3240
            .map(|effect_buffer| effect_buffer.max_binding_source())
×
3241
        else {
3242
            // Since we failed to resolve, remove this component so the next systems ignore
3243
            // this effect.
3244
            warn!(
×
3245
                "Unknown parent buffer #{} on entity {:?}, removing CachedChildInfo.",
×
3246
                parent_cached_effect.buffer_index, child_entity
3247
            );
3248
            commands.entity(child_entity).remove::<CachedChildInfo>();
×
3249
            continue;
×
3250
        };
3251

3252
        let Some(child_event_buffer) = event_cache.get_buffer(cached_effect_events.buffer_index)
×
3253
        else {
3254
            // Since we failed to resolve, remove this component so the next systems ignore
3255
            // this effect.
3256
            warn!(
×
3257
                "Unknown child event buffer #{} on entity {:?}, removing CachedChildInfo.",
×
3258
                cached_effect_events.buffer_index, child_entity
3259
            );
3260
            commands.entity(child_entity).remove::<CachedChildInfo>();
×
3261
            continue;
×
3262
        };
3263
        let child_buffer_binding_source = BufferBindingSource {
3264
            buffer: child_event_buffer.clone(),
3265
            offset: cached_effect_events.range.start,
3266
            size: NonZeroU32::new(cached_effect_events.range.len() as u32).unwrap(),
3267
        };
3268

3269
        // Push the child entity into the children list
3270
        let (child_vec, child_infos) = children_from_parent.entry(parent_entity).or_default();
3271
        let local_child_index = child_vec.len() as u32;
3272
        child_vec.push((child_entity, child_buffer_binding_source));
3273
        child_infos.push(GpuChildInfo {
3274
            event_count: 0,
3275
            init_indirect_dispatch_index: cached_effect_events.init_indirect_dispatch_index,
3276
        });
3277

3278
        // Check if child info changed. Avoid overwriting if no change.
3279
        if let Some(old_cached_child_info) = cached_child_info {
×
3280
            if parent_entity == old_cached_child_info.parent
3281
                && parent_cached_effect.slice.particle_layout
×
3282
                    == old_cached_child_info.parent_particle_layout
×
3283
                && parent_buffer_binding_source
×
3284
                    == old_cached_child_info.parent_buffer_binding_source
×
3285
                // 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.
3286
                && local_child_index == old_cached_child_info.local_child_index
×
3287
                && cached_effect_events.init_indirect_dispatch_index
×
3288
                    == old_cached_child_info.init_indirect_dispatch_index
×
3289
            {
3290
                trace!(
×
3291
                    "ChildInfo didn't change for child entity {:?}, skipping component write.",
×
3292
                    child_entity
3293
                );
3294
                continue;
×
3295
            }
3296
        }
3297

3298
        // Allocate (or overwrite, if already existing) the child info, now that the
3299
        // parent is resolved.
3300
        let cached_child_info = CachedChildInfo {
3301
            parent: parent_entity,
3302
            parent_particle_layout: parent_cached_effect.slice.particle_layout.clone(),
×
3303
            parent_buffer_binding_source,
3304
            local_child_index,
3305
            global_child_index: u32::MAX, // fixed up later by fixup_parents()
3306
            init_indirect_dispatch_index: cached_effect_events.init_indirect_dispatch_index,
×
3307
        };
3308
        commands.entity(child_entity).insert(cached_child_info);
×
3309
        trace!("Spawned CachedChildInfo on child entity {:?}", child_entity);
×
3310

3311
        // Make a note of the parent entity so that we remember to mark its
3312
        // `CachedParentInfo` as changed below.
3313
        parents_with_dirty_children.insert(parent_entity);
3314
    }
3315

3316
    // Once all parents are resolved, diff all children of already-cached parents,
3317
    // and re-allocate their GpuChildInfo if needed.
3318
    for (parent_entity, mut cached_parent_info) in q_parent_effects.iter_mut() {
×
3319
        // Fetch the newly extracted list of children
3320
        let Some((_, (children, child_infos))) = children_from_parent.remove_entry(&parent_entity)
×
3321
        else {
3322
            trace!("Entity {parent_entity:?} is no more a parent, removing CachedParentInfo component...");
×
3323
            commands.entity(parent_entity).remove::<CachedParentInfo>();
×
3324
            continue;
×
3325
        };
3326

3327
        // If we updated `CachedChildInfo` for any of this entity's children,
3328
        // then even if the check below passes, we must still set the change
3329
        // flag on this entity's `CachedParentInfo`. That's because the
3330
        // `fixup_parents` system looks at the change flag for the parent in
3331
        // order to determine which `CachedChildInfo` it needs to update, and
3332
        // that system must process all newly-added `CachedChildInfo`s.
3333
        if parents_with_dirty_children.contains(&parent_entity) {
×
3334
            cached_parent_info.set_changed();
×
3335
        }
3336

3337
        // Check if any child changed compared to the existing CachedChildren component
3338
        if !is_child_list_changed(
3339
            parent_entity,
3340
            cached_parent_info
3341
                .children
3342
                .iter()
3343
                .map(|(entity, _)| *entity),
×
3344
            children.iter().map(|(entity, _)| *entity),
×
3345
        ) {
3346
            continue;
×
3347
        }
3348

3349
        event_cache.reallocate_child_infos(
×
3350
            parent_entity,
×
3351
            children,
×
3352
            &child_infos[..],
×
3353
            cached_parent_info.deref_mut(),
×
3354
        );
3355
    }
3356

3357
    // Once this is done, the children hash map contains all entries which don't
3358
    // already have a CachedParentInfo component. That is, all entities which are
3359
    // new parents.
3360
    for (parent_entity, (children, child_infos)) in children_from_parent.drain() {
×
3361
        let cached_parent_info =
3362
            event_cache.allocate_child_infos(parent_entity, children, &child_infos[..]);
3363
        commands.entity(parent_entity).insert(cached_parent_info);
3364
    }
3365

3366
    // // Once all changes are applied, immediately schedule any GPU buffer
3367
    // // (re)allocation based on the new buffer size. The actual GPU buffer
3368
    // content // will be written later.
3369
    // if event_cache
3370
    //     .child_infos()
3371
    //     .allocate_gpu(render_device, render_queue)
3372
    // {
3373
    //     // All those bind groups use the buffer so need to be re-created
3374
    //     effect_bind_groups.particle_buffers.clear();
3375
    // }
3376
}
3377

3378
pub fn fixup_parents(
×
3379
    q_changed_parents: Query<(Entity, &CachedParentInfo), Changed<CachedParentInfo>>,
3380
    mut q_children: Query<&mut CachedChildInfo>,
3381
) {
3382
    #[cfg(feature = "trace")]
3383
    let _span = bevy::utils::tracing::info_span!("fixup_parents").entered();
×
3384
    trace!("fixup_parents");
×
3385

3386
    // Once all parents are (re-)allocated, fix up the global index of all
3387
    // children if the parent base index changed.
3388
    trace!(
×
3389
        "Updating the global index of children of parent effects whose child list just changed..."
×
3390
    );
3391
    for (parent_entity, cached_parent_info) in q_changed_parents.iter() {
×
3392
        let base_index =
3393
            cached_parent_info.byte_range.start / GpuChildInfo::SHADER_SIZE.get() as u32;
3394
        trace!(
3395
            "Updating {} children of parent effect {:?} with base child index {}...",
×
3396
            cached_parent_info.children.len(),
×
3397
            parent_entity,
3398
            base_index
3399
        );
3400
        for (child_entity, _) in &cached_parent_info.children {
×
3401
            let Ok(mut cached_child_info) = q_children.get_mut(*child_entity) else {
×
3402
                continue;
×
3403
            };
3404
            cached_child_info.global_child_index = base_index + cached_child_info.local_child_index;
×
3405
            trace!(
×
3406
                "+ Updated global index for child ID {:?} of parent {:?}: local={}, global={}",
×
3407
                child_entity,
×
3408
                parent_entity,
×
3409
                cached_child_info.local_child_index,
×
3410
                cached_child_info.global_child_index
×
3411
            );
3412
        }
3413
    }
3414
}
3415

3416
// TEMP - Mark all cached effects as invalid for this frame until another system
3417
// explicitly marks them as valid. Otherwise we early out in some parts, and
3418
// reuse by mistake the previous frame's extraction.
3419
pub fn clear_all_effects(
×
3420
    mut commands: Commands,
3421
    mut q_cached_effects: Query<Entity, With<BatchInput>>,
3422
) {
3423
    for entity in &mut q_cached_effects {
×
3424
        if let Some(mut cmd) = commands.get_entity(entity) {
×
3425
            cmd.remove::<BatchInput>();
3426
        }
3427
    }
3428
}
3429

3430
/// Indexed mesh metadata for [`CachedMesh`].
3431
#[derive(Debug, Clone)]
3432
#[allow(dead_code)]
3433
pub(crate) struct MeshIndexSlice {
3434
    /// Index format.
3435
    pub format: IndexFormat,
3436
    /// GPU buffer containing the indices.
3437
    pub buffer: Buffer,
3438
    /// Range inside [`Self::buffer`] where the indices are.
3439
    pub range: Range<u32>,
3440
}
3441

3442
/// Render world cached mesh infos for a single effect instance.
3443
#[derive(Debug, Clone, Component)]
3444
pub(crate) struct CachedMesh {
3445
    /// Asset of the effect mesh to draw.
3446
    pub mesh: AssetId<Mesh>,
3447
    /// GPU buffer storing the [`mesh`] of the effect.
3448
    pub buffer: Buffer,
3449
    /// Range slice inside the GPU buffer for the effect mesh.
3450
    pub range: Range<u32>,
3451
    /// Indexed rendering metadata.
3452
    #[allow(unused)]
3453
    pub indexed: Option<MeshIndexSlice>,
3454
}
3455

3456
/// Render world cached properties info for a single effect instance.
3457
#[allow(unused)]
3458
#[derive(Debug, Component)]
3459
pub(crate) struct CachedProperties {
3460
    /// Layout of the effect properties.
3461
    pub layout: PropertyLayout,
3462
    /// Index of the buffer in the [`EffectCache`].
3463
    pub buffer_index: u32,
3464
    /// Offset in bytes inside the buffer.
3465
    pub offset: u32,
3466
    /// Binding size in bytes of the property struct.
3467
    pub binding_size: u32,
3468
}
3469

3470
#[derive(SystemParam)]
3471
pub struct PrepareEffectsReadOnlyParams<'w, 's> {
3472
    sim_params: Res<'w, SimParams>,
3473
    render_device: Res<'w, RenderDevice>,
3474
    render_queue: Res<'w, RenderQueue>,
3475
    #[system_param(ignore)]
3476
    marker: PhantomData<&'s usize>,
3477
}
3478

3479
#[derive(SystemParam)]
3480
pub struct PipelineSystemParams<'w, 's> {
3481
    pipeline_cache: Res<'w, PipelineCache>,
3482
    init_pipeline: ResMut<'w, ParticlesInitPipeline>,
3483
    indirect_pipeline: Res<'w, DispatchIndirectPipeline>,
3484
    update_pipeline: ResMut<'w, ParticlesUpdatePipeline>,
3485
    specialized_init_pipelines: ResMut<'w, SpecializedComputePipelines<ParticlesInitPipeline>>,
3486
    specialized_update_pipelines: ResMut<'w, SpecializedComputePipelines<ParticlesUpdatePipeline>>,
3487
    specialized_indirect_pipelines:
3488
        ResMut<'w, SpecializedComputePipelines<DispatchIndirectPipeline>>,
3489
    #[system_param(ignore)]
3490
    marker: PhantomData<&'s usize>,
3491
}
3492

3493
pub(crate) fn prepare_effects(
×
3494
    mut commands: Commands,
3495
    read_only_params: PrepareEffectsReadOnlyParams,
3496
    mut pipelines: PipelineSystemParams,
3497
    mut property_cache: ResMut<PropertyCache>,
3498
    event_cache: Res<EventCache>,
3499
    mut effect_cache: ResMut<EffectCache>,
3500
    mut effects_meta: ResMut<EffectsMeta>,
3501
    mut effect_bind_groups: ResMut<EffectBindGroups>,
3502
    mut extracted_effects: ResMut<ExtractedEffects>,
3503
    mut property_bind_groups: ResMut<PropertyBindGroups>,
3504
    q_cached_effects: Query<(
3505
        MainEntity,
3506
        &CachedEffect,
3507
        Ref<CachedMesh>,
3508
        &DispatchBufferIndices,
3509
        Option<&CachedEffectProperties>,
3510
        Option<&CachedParentInfo>,
3511
        Option<&CachedChildInfo>,
3512
        Option<&CachedEffectEvents>,
3513
    )>,
3514
    q_debug_all_entities: Query<MainEntity>,
3515
    mut gpu_buffer_operations: ResMut<GpuBufferOperations>,
3516
    mut sort_bind_groups: ResMut<SortBindGroups>,
3517
    mut init_fill_dispatch_queue: ResMut<InitFillDispatchQueue>,
3518
) {
3519
    #[cfg(feature = "trace")]
3520
    let _span = bevy::utils::tracing::info_span!("prepare_effects").entered();
×
3521
    trace!("prepare_effects");
×
3522

3523
    init_fill_dispatch_queue.clear();
×
3524

3525
    // Workaround for too many params in system (TODO: refactor to split work?)
3526
    let sim_params = read_only_params.sim_params.into_inner();
×
3527
    let render_device = read_only_params.render_device.into_inner();
×
3528
    let render_queue = read_only_params.render_queue.into_inner();
×
3529
    let pipeline_cache = pipelines.pipeline_cache.into_inner();
×
3530
    let specialized_init_pipelines = pipelines.specialized_init_pipelines.into_inner();
×
3531
    let specialized_update_pipelines = pipelines.specialized_update_pipelines.into_inner();
×
3532
    let specialized_indirect_pipelines = pipelines.specialized_indirect_pipelines.into_inner();
×
3533

3534
    // // sort first by z and then by handle. this ensures that, when possible,
3535
    // batches span multiple z layers // batches won't span z-layers if there is
3536
    // another batch between them extracted_effects.effects.sort_by(|a, b| {
3537
    //     match FloatOrd(a.transform.w_axis[2]).cmp(&FloatOrd(b.transform.
3538
    // w_axis[2])) {         Ordering::Equal => a.handle.cmp(&b.handle),
3539
    //         other => other,
3540
    //     }
3541
    // });
3542

3543
    // Ensure the indirect pipelines are created
3544
    if effects_meta.indirect_pipeline_ids[0] == CachedComputePipelineId::INVALID {
×
3545
        effects_meta.indirect_pipeline_ids[0] = specialized_indirect_pipelines.specialize(
×
3546
            pipeline_cache,
×
3547
            &pipelines.indirect_pipeline,
×
3548
            DispatchIndirectPipelineKey { has_events: false },
×
3549
        );
3550
    }
3551
    if effects_meta.indirect_pipeline_ids[1] == CachedComputePipelineId::INVALID {
×
3552
        effects_meta.indirect_pipeline_ids[1] = specialized_indirect_pipelines.specialize(
×
3553
            pipeline_cache,
×
3554
            &pipelines.indirect_pipeline,
×
3555
            DispatchIndirectPipelineKey { has_events: true },
×
3556
        );
3557
    }
3558
    if effects_meta.active_indirect_pipeline_id == CachedComputePipelineId::INVALID {
×
3559
        effects_meta.active_indirect_pipeline_id = effects_meta.indirect_pipeline_ids[0];
×
3560
    } else {
3561
        // If this is the first time we insert an event buffer, we need to switch the
3562
        // indirect pass from non-event to event mode. That is, we need to re-allocate
3563
        // the pipeline with the child infos buffer binding. Conversely, if there's no
3564
        // more effect using GPU spawn events, we can deallocate.
3565
        let was_empty =
×
3566
            effects_meta.active_indirect_pipeline_id == effects_meta.indirect_pipeline_ids[0];
×
3567
        let is_empty = event_cache.child_infos().is_empty();
×
3568
        if was_empty && !is_empty {
×
3569
            trace!("First event buffer inserted; switching indirect pass to event mode...");
×
3570
            effects_meta.active_indirect_pipeline_id = effects_meta.indirect_pipeline_ids[1];
×
3571
        } else if is_empty && !was_empty {
×
3572
            trace!("Last event buffer removed; switching indirect pass to no-event mode...");
×
3573
            effects_meta.active_indirect_pipeline_id = effects_meta.indirect_pipeline_ids[0];
×
3574
        }
3575
    }
3576

3577
    gpu_buffer_operations.begin_frame();
×
3578

3579
    // Clear per-instance buffers, which are filled below and re-uploaded each frame
3580
    effects_meta.spawner_buffer.clear();
×
3581

3582
    // Build batcher inputs from extracted effects, updating all cached components
3583
    // for each effect on the fly.
3584
    let effects = std::mem::take(&mut extracted_effects.effects);
×
3585
    let extracted_effect_count = effects.len();
×
3586
    let mut prepared_effect_count = 0;
×
3587
    for extracted_effect in effects.into_iter() {
×
3588
        // Skip effects not cached. Since we're iterating over the extracted effects
3589
        // instead of the cached ones, it might happen we didn't cache some effect on
3590
        // purpose because they failed earlier validations.
3591
        // FIXME - extract into ECS directly so we don't have to do that?
3592
        let Ok((
3593
            main_entity,
×
3594
            cached_effect,
×
3595
            cached_mesh,
×
3596
            dispatch_buffer_indices,
×
3597
            cached_effect_properties,
×
3598
            cached_parent_info,
×
3599
            cached_child_info,
×
3600
            cached_effect_events,
×
3601
        )) = q_cached_effects.get(extracted_effect.render_entity.id())
×
3602
        else {
3603
            warn!(
×
3604
                "Unknown render entity {:?} for extracted effect.",
×
3605
                extracted_effect.render_entity.id()
×
3606
            );
3607
            if let Ok(main_entity) = q_debug_all_entities.get(extracted_effect.render_entity.id()) {
×
3608
                info!(
3609
                    "Render entity {:?} exists with main entity {:?}, some component missing!",
×
3610
                    extracted_effect.render_entity.id(),
×
3611
                    main_entity
3612
                );
3613
            } else {
3614
                info!(
×
3615
                    "Render entity {:?} does not exists with a MainEntity.",
×
3616
                    extracted_effect.render_entity.id()
×
3617
                );
3618
            }
3619
            continue;
×
3620
        };
3621

3622
        let effect_slice = EffectSlice {
3623
            slice: cached_effect.slice.range(),
3624
            buffer_index: cached_effect.buffer_index,
3625
            particle_layout: cached_effect.slice.particle_layout.clone(),
3626
        };
3627

3628
        let has_event_buffer = cached_child_info.is_some();
3629
        // FIXME: decouple "consumes event" from "reads parent particle" (here, p.layout
3630
        // should be Option<T>, not T)
3631
        let property_layout_min_binding_size = if extracted_effect.property_layout.is_empty() {
3632
            None
×
3633
        } else {
3634
            Some(extracted_effect.property_layout.min_binding_size())
×
3635
        };
3636

3637
        // Schedule some GPU buffer operation to update the number of workgroups to
3638
        // dispatch during the indirect init pass of this effect based on the number of
3639
        // GPU spawn events written in its buffer.
3640
        if let (Some(cached_effect_events), Some(cached_child_info)) =
×
3641
            (cached_effect_events, cached_child_info)
3642
        {
3643
            debug_assert_eq!(
3644
                GpuChildInfo::min_size().get() % 4,
3645
                0,
3646
                "Invalid GpuChildInfo alignment."
×
3647
            );
3648

3649
            // Resolve parent entry
3650
            let Ok((_, _, _, _, _, cached_parent_info, _, _)) =
×
3651
                q_cached_effects.get(cached_child_info.parent)
×
3652
            else {
3653
                continue;
×
3654
            };
3655
            let Some(cached_parent_info) = cached_parent_info else {
×
3656
                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);
×
3657
                continue;
×
3658
            };
3659

3660
            let init_indirect_dispatch_index = cached_effect_events.init_indirect_dispatch_index;
3661
            assert_eq!(0, cached_parent_info.byte_range.start % 4);
3662
            let global_child_index = cached_child_info.global_child_index;
×
3663

3664
            // Schedule a fill dispatch
3665
            trace!(
×
3666
                "init_fill_dispatch.push(): src:global_child_index={} dst:init_indirect_dispatch_index={}",
×
3667
                global_child_index,
3668
                init_indirect_dispatch_index,
3669
            );
3670
            init_fill_dispatch_queue.enqueue(global_child_index, init_indirect_dispatch_index);
×
3671
        }
3672

3673
        // Create init pipeline key flags.
3674
        let init_pipeline_key_flags = {
×
3675
            let mut flags = ParticleInitPipelineKeyFlags::empty();
×
3676
            flags.set(
×
3677
                ParticleInitPipelineKeyFlags::ATTRIBUTE_PREV,
×
3678
                effect_slice.particle_layout.contains(Attribute::PREV),
×
3679
            );
3680
            flags.set(
×
3681
                ParticleInitPipelineKeyFlags::ATTRIBUTE_NEXT,
×
3682
                effect_slice.particle_layout.contains(Attribute::NEXT),
×
3683
            );
3684
            flags.set(
×
3685
                ParticleInitPipelineKeyFlags::CONSUME_GPU_SPAWN_EVENTS,
×
3686
                has_event_buffer,
×
3687
            );
3688
            flags
×
3689
        };
3690

3691
        // This should always exist by the time we reach this point, because we should
3692
        // have inserted any property in the cache, which would have allocated the
3693
        // proper bind group layout (or the default no-property one).
3694
        let spawner_bind_group_layout = property_cache
×
3695
            .bind_group_layout(property_layout_min_binding_size)
×
3696
            .unwrap_or_else(|| {
×
3697
                panic!(
×
3698
                    "Failed to find spawner@2 bind group layout for property binding size {:?}",
×
3699
                    property_layout_min_binding_size,
×
3700
                )
3701
            });
3702
        trace!(
3703
            "Retrieved spawner@2 bind group layout {:?} for property binding size {:?}.",
×
3704
            spawner_bind_group_layout.id(),
×
3705
            property_layout_min_binding_size
3706
        );
3707

3708
        // Fetch the bind group layouts from the cache
3709
        trace!("cached_child_info={:?}", cached_child_info);
×
3710
        let (parent_particle_layout_min_binding_size, parent_buffer_index) =
×
3711
            if let Some(cached_child) = cached_child_info.as_ref() {
×
3712
                let Ok((_, parent_cached_effect, _, _, _, _, _, _)) =
×
3713
                    q_cached_effects.get(cached_child.parent)
3714
                else {
3715
                    // At this point we should have discarded invalid effects with a missing parent,
3716
                    // so if the parent is not found this is a bug.
3717
                    error!(
×
3718
                        "Effect main_entity {:?}: parent render entity {:?} not found.",
×
3719
                        main_entity, cached_child.parent
3720
                    );
3721
                    continue;
×
3722
                };
3723
                (
3724
                    Some(
3725
                        parent_cached_effect
3726
                            .slice
3727
                            .particle_layout
3728
                            .min_binding_size32(),
3729
                    ),
3730
                    Some(parent_cached_effect.buffer_index),
3731
                )
3732
            } else {
3733
                (None, None)
×
3734
            };
3735
        let Some(particle_bind_group_layout) = effect_cache.particle_bind_group_layout(
×
3736
            effect_slice.particle_layout.min_binding_size32(),
3737
            parent_particle_layout_min_binding_size,
3738
        ) else {
3739
            error!("Failed to find particle sim bind group @1 for min_binding_size={} parent_min_binding_size={:?}", 
×
3740
            effect_slice.particle_layout.min_binding_size32(), parent_particle_layout_min_binding_size);
×
3741
            continue;
×
3742
        };
3743
        let particle_bind_group_layout = particle_bind_group_layout.clone();
3744
        trace!(
3745
            "Retrieved particle@1 bind group layout {:?} for particle binding size {:?} and parent binding size {:?}.",
×
3746
            particle_bind_group_layout.id(),
×
3747
            effect_slice.particle_layout.min_binding_size32(),
×
3748
            parent_particle_layout_min_binding_size,
3749
        );
3750

3751
        let particle_layout_min_binding_size = effect_slice.particle_layout.min_binding_size32();
3752
        let spawner_bind_group_layout = spawner_bind_group_layout.clone();
3753

3754
        // Specialize the init pipeline based on the effect.
3755
        let init_pipeline_id = {
3756
            let consume_gpu_spawn_events = init_pipeline_key_flags
3757
                .contains(ParticleInitPipelineKeyFlags::CONSUME_GPU_SPAWN_EVENTS);
3758

3759
            // Fetch the metadata@3 bind group layout from the cache
3760
            let metadata_bind_group_layout = effect_cache
3761
                .metadata_init_bind_group_layout(consume_gpu_spawn_events)
3762
                .unwrap()
3763
                .clone();
3764

3765
            // https://github.com/bevyengine/bevy/issues/17132
3766
            let particle_bind_group_layout_id = particle_bind_group_layout.id();
3767
            let spawner_bind_group_layout_id = spawner_bind_group_layout.id();
3768
            let metadata_bind_group_layout_id = metadata_bind_group_layout.id();
3769
            pipelines.init_pipeline.temp_particle_bind_group_layout =
3770
                Some(particle_bind_group_layout.clone());
3771
            pipelines.init_pipeline.temp_spawner_bind_group_layout =
3772
                Some(spawner_bind_group_layout.clone());
3773
            pipelines.init_pipeline.temp_metadata_bind_group_layout =
3774
                Some(metadata_bind_group_layout);
3775
            let init_pipeline_id: CachedComputePipelineId = specialized_init_pipelines.specialize(
3776
                pipeline_cache,
3777
                &pipelines.init_pipeline,
3778
                ParticleInitPipelineKey {
3779
                    shader: extracted_effect.effect_shaders.init.clone(),
3780
                    particle_layout_min_binding_size,
3781
                    parent_particle_layout_min_binding_size,
3782
                    flags: init_pipeline_key_flags,
3783
                    particle_bind_group_layout_id,
3784
                    spawner_bind_group_layout_id,
3785
                    metadata_bind_group_layout_id,
3786
                },
3787
            );
3788
            // keep things tidy; this is just a hack, should not persist
3789
            pipelines.init_pipeline.temp_particle_bind_group_layout = None;
3790
            pipelines.init_pipeline.temp_spawner_bind_group_layout = None;
3791
            pipelines.init_pipeline.temp_metadata_bind_group_layout = None;
3792
            trace!("Init pipeline specialized: id={:?}", init_pipeline_id);
×
3793

3794
            init_pipeline_id
3795
        };
3796

3797
        let update_pipeline_id = {
3798
            let num_event_buffers = cached_parent_info
3799
                .map(|p| p.children.len() as u32)
×
3800
                .unwrap_or_default();
3801

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

3809
            // Fetch the bind group layouts from the cache
3810
            let metadata_bind_group_layout = effect_cache
3811
                .metadata_update_bind_group_layout(num_event_buffers)
3812
                .unwrap()
3813
                .clone();
3814

3815
            // https://github.com/bevyengine/bevy/issues/17132
3816
            let particle_bind_group_layout_id = particle_bind_group_layout.id();
3817
            let spawner_bind_group_layout_id = spawner_bind_group_layout.id();
3818
            let metadata_bind_group_layout_id = metadata_bind_group_layout.id();
3819
            pipelines.update_pipeline.temp_particle_bind_group_layout =
3820
                Some(particle_bind_group_layout);
3821
            pipelines.update_pipeline.temp_spawner_bind_group_layout =
3822
                Some(spawner_bind_group_layout);
3823
            pipelines.update_pipeline.temp_metadata_bind_group_layout =
3824
                Some(metadata_bind_group_layout);
3825
            let update_pipeline_id = specialized_update_pipelines.specialize(
3826
                pipeline_cache,
3827
                &pipelines.update_pipeline,
3828
                ParticleUpdatePipelineKey {
3829
                    shader: extracted_effect.effect_shaders.update.clone(),
3830
                    particle_layout: effect_slice.particle_layout.clone(),
3831
                    parent_particle_layout_min_binding_size,
3832
                    num_event_buffers,
3833
                    particle_bind_group_layout_id,
3834
                    spawner_bind_group_layout_id,
3835
                    metadata_bind_group_layout_id,
3836
                },
3837
            );
3838
            // keep things tidy; this is just a hack, should not persist
3839
            pipelines.update_pipeline.temp_particle_bind_group_layout = None;
3840
            pipelines.update_pipeline.temp_spawner_bind_group_layout = None;
3841
            pipelines.update_pipeline.temp_metadata_bind_group_layout = None;
3842
            trace!("Update pipeline specialized: id={:?}", update_pipeline_id);
×
3843

3844
            update_pipeline_id
3845
        };
3846

3847
        let init_and_update_pipeline_ids = InitAndUpdatePipelineIds {
3848
            init: init_pipeline_id,
3849
            update: update_pipeline_id,
3850
        };
3851

3852
        // For ribbons, which need particle sorting, create a bind group layout for
3853
        // sorting the effect, based on its particle layout.
3854
        if extracted_effect.layout_flags.contains(LayoutFlags::RIBBONS) {
3855
            if let Err(err) = sort_bind_groups.ensure_sort_fill_bind_group_layout(
×
3856
                pipeline_cache,
×
3857
                &extracted_effect.particle_layout,
×
3858
            ) {
3859
                error!(
3860
                    "Failed to create bind group for ribbon effect sorting: {:?}",
×
3861
                    err
3862
                );
3863
                continue;
3864
            }
3865
        }
3866

3867
        // Output some debug info
3868
        trace!("init_shader = {:?}", extracted_effect.effect_shaders.init);
×
3869
        trace!(
3870
            "update_shader = {:?}",
×
3871
            extracted_effect.effect_shaders.update
3872
        );
3873
        trace!(
3874
            "render_shader = {:?}",
×
3875
            extracted_effect.effect_shaders.render
3876
        );
3877
        trace!("layout_flags = {:?}", extracted_effect.layout_flags);
×
3878
        trace!("particle_layout = {:?}", effect_slice.particle_layout);
×
3879

3880
        let spawner_index = effects_meta.allocate_spawner(
3881
            &extracted_effect.transform,
3882
            extracted_effect.spawn_count,
3883
            extracted_effect.prng_seed,
3884
            dispatch_buffer_indices.effect_metadata_buffer_table_id,
3885
        );
3886

3887
        trace!(
3888
            "Updating cached effect at entity {:?}...",
×
3889
            extracted_effect.render_entity.id()
×
3890
        );
3891
        let mut cmd = commands.entity(extracted_effect.render_entity.id());
3892
        cmd.insert(BatchInput {
3893
            handle: extracted_effect.handle,
3894
            entity: extracted_effect.render_entity.id(),
3895
            main_entity: extracted_effect.main_entity,
3896
            effect_slice,
3897
            init_and_update_pipeline_ids,
3898
            parent_buffer_index,
3899
            event_buffer_index: cached_effect_events.map(|cee| cee.buffer_index),
×
3900
            child_effects: cached_parent_info
3901
                .map(|cp| cp.children.clone())
×
3902
                .unwrap_or_default(),
3903
            layout_flags: extracted_effect.layout_flags,
3904
            texture_layout: extracted_effect.texture_layout.clone(),
3905
            textures: extracted_effect.textures.clone(),
3906
            alpha_mode: extracted_effect.alpha_mode,
3907
            particle_layout: extracted_effect.particle_layout.clone(),
3908
            shaders: extracted_effect.effect_shaders,
3909
            spawner_index,
3910
            spawn_count: extracted_effect.spawn_count,
3911
            position: extracted_effect.transform.translation(),
3912
            init_indirect_dispatch_index: cached_child_info
3913
                .map(|cc| cc.init_indirect_dispatch_index),
×
3914
        });
3915

3916
        // Update properties
3917
        if let Some(cached_effect_properties) = cached_effect_properties {
×
3918
            // Because the component is persisted, it may be there from a previous version
3919
            // of the asset. And add_remove_effects() only add new instances or remove old
3920
            // ones, but doesn't update existing ones. Check if it needs to be removed.
3921
            // FIXME - Dedupe with add_remove_effect(), we shouldn't have 2 codepaths doing
3922
            // the same thing at 2 different times.
3923
            if extracted_effect.property_layout.is_empty() {
3924
                trace!(
×
3925
                    "Render entity {:?} had CachedEffectProperties component, but newly extracted property layout is empty. Removing component...",
×
3926
                    extracted_effect.render_entity.id(),
×
3927
                );
3928
                cmd.remove::<CachedEffectProperties>();
×
3929
                // Also remove the other one. FIXME - dedupe those two...
3930
                cmd.remove::<CachedProperties>();
×
3931

3932
                if extracted_effect.property_data.is_some() {
×
3933
                    warn!(
×
3934
                        "Effect on entity {:?} doesn't declare any property in its Module, but some property values were provided. Those values will be discarded.",
×
3935
                        extracted_effect.main_entity.id(),
×
3936
                    );
3937
                }
3938
            } else {
3939
                // Insert a new component or overwrite the existing one
3940
                cmd.insert(CachedProperties {
×
3941
                    layout: extracted_effect.property_layout.clone(),
×
3942
                    buffer_index: cached_effect_properties.buffer_index,
×
3943
                    offset: cached_effect_properties.range.start,
×
3944
                    binding_size: cached_effect_properties.range.len() as u32,
×
3945
                });
3946

3947
                // Write properties for this effect if they were modified.
3948
                // FIXME - This doesn't work with batching!
3949
                if let Some(property_data) = &extracted_effect.property_data {
×
3950
                    trace!(
3951
                    "Properties changed; (re-)uploading to GPU... New data: {} bytes. Capacity: {} bytes.",
×
3952
                    property_data.len(),
×
3953
                    cached_effect_properties.range.len(),
×
3954
                );
3955
                    if property_data.len() <= cached_effect_properties.range.len() {
×
3956
                        let property_buffer = property_cache.buffers_mut()
×
3957
                            [cached_effect_properties.buffer_index as usize]
×
3958
                            .as_mut()
3959
                            .unwrap();
3960
                        property_buffer.write(cached_effect_properties.range.start, property_data);
×
3961
                    } else {
3962
                        error!(
×
3963
                            "Cannot upload properties: existing property slice in property buffer #{} is too small ({} bytes) for the new data ({} bytes).",
×
3964
                            cached_effect_properties.buffer_index,
×
3965
                            cached_effect_properties.range.len(),
×
3966
                            property_data.len()
×
3967
                        );
3968
                    }
3969
                }
3970
            }
3971
        } else {
3972
            // No property on the effect; remove the component
3973
            trace!(
×
3974
                "No CachedEffectProperties on render entity {:?}, remove any CachedProperties component too.",
×
3975
                extracted_effect.render_entity.id()
×
3976
            );
3977
            cmd.remove::<CachedProperties>();
×
3978
        }
3979

3980
        // Now that the effect is entirely prepared and all GPU resources are allocated,
3981
        // update its GpuEffectMetadata with all those infos.
3982
        // FIXME - should do this only when the below changes (not only the mesh), via
3983
        // some invalidation mechanism and ECS change detection.
3984
        if cached_mesh.is_changed() {
3985
            let capacity = cached_effect.slice.len();
×
3986

3987
            // Global and local indices of this effect as a child of another (parent) effect
3988
            let (global_child_index, local_child_index) = cached_child_info
×
3989
                .map(|cci| (cci.global_child_index, cci.local_child_index))
×
3990
                .unwrap_or_default();
3991

3992
            // Base index of all children of this (parent) effect
3993
            let base_child_index = cached_parent_info
×
3994
                .map(|cpi| {
×
3995
                    debug_assert_eq!(
×
3996
                        cpi.byte_range.start % GpuChildInfo::SHADER_SIZE.get() as u32,
×
3997
                        0
3998
                    );
3999
                    cpi.byte_range.start / GpuChildInfo::SHADER_SIZE.get() as u32
×
4000
                })
4001
                .unwrap_or_default();
4002

4003
            let particle_stride = extracted_effect.particle_layout.min_binding_size32().get() / 4;
×
4004
            let sort_key_offset = extracted_effect
×
4005
                .particle_layout
×
4006
                .offset(Attribute::RIBBON_ID)
×
4007
                .unwrap_or(0)
×
4008
                / 4;
×
4009
            let sort_key2_offset = extracted_effect
×
4010
                .particle_layout
×
4011
                .offset(Attribute::AGE)
×
4012
                .unwrap_or(0)
×
4013
                / 4;
×
4014

4015
            let mut gpu_effect_metadata = GpuEffectMetadata {
4016
                instance_count: 0,
4017
                base_instance: 0,
4018
                alive_count: 0,
4019
                max_update: 0,
4020
                dead_count: capacity,
4021
                max_spawn: capacity,
4022
                ping: 0,
4023
                indirect_dispatch_index: dispatch_buffer_indices
×
4024
                    .update_dispatch_indirect_buffer_row_index,
4025
                // Note: the indirect draw args are at the start of the GpuEffectMetadata struct
4026
                indirect_render_index: dispatch_buffer_indices.effect_metadata_buffer_table_id.0,
×
4027
                init_indirect_dispatch_index: cached_effect_events
×
4028
                    .map(|cee| cee.init_indirect_dispatch_index)
4029
                    .unwrap_or_default(),
4030
                local_child_index,
4031
                global_child_index,
4032
                base_child_index,
4033
                particle_stride,
4034
                sort_key_offset,
4035
                sort_key2_offset,
4036
                ..default()
4037
            };
4038
            if let Some(indexed) = &cached_mesh.indexed {
×
4039
                gpu_effect_metadata.vertex_or_index_count = indexed.range.len() as u32;
4040
                gpu_effect_metadata.first_index_or_vertex_offset = indexed.range.start;
4041
                gpu_effect_metadata.vertex_offset_or_base_instance = cached_mesh.range.start as i32;
4042
            } else {
4043
                gpu_effect_metadata.vertex_or_index_count = cached_mesh.range.len() as u32;
×
4044
                gpu_effect_metadata.first_index_or_vertex_offset = cached_mesh.range.start;
×
4045
                gpu_effect_metadata.vertex_offset_or_base_instance = 0;
×
4046
            };
4047
            assert!(dispatch_buffer_indices
×
4048
                .effect_metadata_buffer_table_id
×
4049
                .is_valid());
×
4050
            effects_meta.effect_metadata_buffer.update(
×
4051
                dispatch_buffer_indices.effect_metadata_buffer_table_id,
×
4052
                gpu_effect_metadata,
×
4053
            );
4054

4055
            warn!(
×
4056
                "Updated metadata entry {} for effect {:?}, this will reset it.",
×
4057
                dispatch_buffer_indices.effect_metadata_buffer_table_id.0, main_entity
4058
            );
4059
        }
4060

4061
        prepared_effect_count += 1;
×
4062
    }
4063
    trace!("Prepared {prepared_effect_count}/{extracted_effect_count} extracted effect(s)");
×
4064

4065
    // Once all EffectMetadata values are written, schedule a GPU upload
4066
    if effects_meta
4067
        .effect_metadata_buffer
4068
        .allocate_gpu(render_device, render_queue)
4069
    {
4070
        // All those bind groups use the buffer so need to be re-created
4071
        trace!("*** Effect metadata buffer re-allocated; clearing all bind groups using it.");
×
4072
        effects_meta.indirect_metadata_bind_group = None;
×
4073
        effect_bind_groups.init_metadata_bind_groups.clear();
×
4074
        effect_bind_groups.update_metadata_bind_groups.clear();
×
4075
    }
4076

4077
    // Write the entire spawner buffer for this frame, for all effects combined
4078
    assert_eq!(
4079
        prepared_effect_count,
4080
        effects_meta.spawner_buffer.len() as u32
4081
    );
4082
    if effects_meta
×
4083
        .spawner_buffer
×
4084
        .write_buffer(render_device, render_queue)
×
4085
    {
4086
        // All property bind groups use the spawner buffer, which was reallocate
NEW
4087
        effect_bind_groups.particle_buffers.clear();
×
4088
        property_bind_groups.clear(true);
×
4089
        effects_meta.indirect_spawner_bind_group = None;
×
4090
    }
4091

4092
    // Update simulation parameters
4093
    effects_meta.sim_params_uniforms.set(sim_params.into());
×
4094
    {
4095
        let gpu_sim_params = effects_meta.sim_params_uniforms.get_mut();
×
4096
        gpu_sim_params.num_effects = prepared_effect_count;
×
4097

4098
        trace!(
×
4099
            "Simulation parameters: time={} delta_time={} virtual_time={} \
×
4100
                virtual_delta_time={} real_time={} real_delta_time={} num_effects={}",
×
4101
            gpu_sim_params.time,
4102
            gpu_sim_params.delta_time,
4103
            gpu_sim_params.virtual_time,
4104
            gpu_sim_params.virtual_delta_time,
4105
            gpu_sim_params.real_time,
4106
            gpu_sim_params.real_delta_time,
4107
            gpu_sim_params.num_effects,
4108
        );
4109
    }
4110
    let prev_buffer_id = effects_meta.sim_params_uniforms.buffer().map(|b| b.id());
×
4111
    effects_meta
×
4112
        .sim_params_uniforms
×
4113
        .write_buffer(render_device, render_queue);
×
4114
    if prev_buffer_id != effects_meta.sim_params_uniforms.buffer().map(|b| b.id()) {
×
4115
        // Buffer changed, invalidate bind groups
4116
        effects_meta.indirect_sim_params_bind_group = None;
×
4117
    }
4118
}
4119

4120
pub(crate) fn batch_effects(
×
4121
    mut commands: Commands,
4122
    effects_meta: Res<EffectsMeta>,
4123
    mut sort_bind_groups: ResMut<SortBindGroups>,
4124
    mut q_cached_effects: Query<(
4125
        Entity,
4126
        &CachedMesh,
4127
        Option<&CachedEffectEvents>,
4128
        Option<&CachedChildInfo>,
4129
        Option<&CachedProperties>,
4130
        &mut DispatchBufferIndices,
4131
        &mut BatchInput,
4132
    )>,
4133
    mut sorted_effect_batches: ResMut<SortedEffectBatches>,
4134
    mut gpu_buffer_operations: ResMut<GpuBufferOperations>,
4135
) {
4136
    trace!("batch_effects");
×
4137

4138
    // Sort first by effect buffer index, then by slice range (see EffectSlice)
4139
    // inside that buffer. This is critical for batching to work, because
4140
    // batching effects is based on compatible items, which implies same GPU
4141
    // buffer and continuous slice ranges (the next slice start must be equal to
4142
    // the previous start end, without gap). EffectSlice already contains both
4143
    // information, and the proper ordering implementation.
4144
    // effect_entity_list.sort_by_key(|a| a.effect_slice.clone());
4145

4146
    // For now we re-create that buffer each frame. Since there's no CPU -> GPU
4147
    // transfer, this is pretty cheap in practice.
4148
    sort_bind_groups.clear_indirect_dispatch_buffer();
×
4149

4150
    let mut sort_queue = GpuBufferOperationQueue::new();
×
4151

4152
    // Loop on all extracted effects in order, and try to batch them together to
4153
    // reduce draw calls. -- currently does nothing, batching was broken and never
4154
    // fixed.
4155
    // FIXME - This is in ECS order, if we re-add the sorting above we need a
4156
    // different order here!
4157
    trace!("Batching {} effects...", q_cached_effects.iter().len());
×
4158
    sorted_effect_batches.clear();
×
4159
    for (
4160
        entity,
×
4161
        cached_mesh,
×
4162
        cached_effect_events,
×
4163
        cached_child_info,
×
4164
        cached_properties,
×
4165
        dispatch_buffer_indices,
×
4166
        mut input,
×
4167
    ) in &mut q_cached_effects
×
4168
    {
4169
        // Detect if this cached effect was not updated this frame by a new extracted
4170
        // effect. This happens when e.g. the effect is invisible and not simulated, or
4171
        // some error prevented it from being extracted. We use the pipeline IDs vector
4172
        // as a marker, because each frame we move it out of the CachedGroup
4173
        // component during batching, so if empty this means a new one was not created
4174
        // this frame.
4175
        // if input.init_and_update_pipeline_ids.is_empty() {
4176
        //     trace!(
4177
        //         "Skipped cached effect on render entity {:?}: not extracted this
4178
        // frame.",         entity
4179
        //     );
4180
        //     continue;
4181
        // }
4182

4183
        let translation = input.position;
×
4184

4185
        // Spawn one EffectBatch per instance (no batching; TODO). This contains
4186
        // most of the data needed to drive rendering. However this doesn't drive
4187
        // rendering; this is just storage.
4188
        let mut effect_batch = EffectBatch::from_input(
4189
            cached_mesh,
×
4190
            cached_effect_events,
×
4191
            cached_child_info,
×
4192
            &mut input,
×
4193
            *dispatch_buffer_indices.as_ref(),
×
4194
            cached_properties.map(|cp| PropertyBindGroupKey {
×
4195
                buffer_index: cp.buffer_index,
×
4196
                binding_size: cp.binding_size,
×
4197
            }),
4198
            cached_properties.map(|cp| cp.offset),
×
4199
        );
4200

4201
        // If the batch has ribbons, we need to sort the particles by RIBBON_ID and AGE
4202
        // for ribbon meshing, in order to avoid gaps when some particles in the middle
4203
        // of the ribbon die (since we can't guarantee a linear lifetime through the
4204
        // ribbon).
4205
        if input.layout_flags.contains(LayoutFlags::RIBBONS) {
×
4206
            // This buffer is allocated in prepare_effects(), so should always be available
4207
            let Some(effect_metadata_buffer) = effects_meta.effect_metadata_buffer.buffer() else {
×
4208
                error!("Failed to find effect metadata buffer. This is a bug.");
×
4209
                continue;
×
4210
            };
4211

4212
            // Allocate a GpuDispatchIndirect entry
4213
            let sort_fill_indirect_dispatch_index = sort_bind_groups.allocate_indirect_dispatch();
4214
            effect_batch.sort_fill_indirect_dispatch_index =
4215
                Some(sort_fill_indirect_dispatch_index);
4216

4217
            // Enqueue a fill dispatch operation which reads GpuEffectMetadata::alive_count,
4218
            // compute a number of workgroups to dispatch based on that particle count, and
4219
            // store the result into a GpuDispatchIndirect struct which will be used to
4220
            // dispatch the fill-sort pass.
4221
            {
4222
                let src_buffer = effect_metadata_buffer.clone();
4223
                let src_binding_offset = effects_meta.effect_metadata_buffer.dynamic_offset(
4224
                    effect_batch
4225
                        .dispatch_buffer_indices
4226
                        .effect_metadata_buffer_table_id,
4227
                );
4228
                let src_binding_size = effects_meta.gpu_limits.effect_metadata_aligned_size;
4229
                let Some(dst_buffer) = sort_bind_groups.indirect_buffer() else {
×
4230
                    error!("Missing indirect dispatch buffer for sorting, cannot schedule particle sort for ribbon. This is a bug.");
×
4231
                    continue;
×
4232
                };
4233
                let dst_buffer = dst_buffer.clone();
4234
                let dst_binding_offset = 0; // see dst_offset below
4235
                                            //let dst_binding_size = NonZeroU32::new(12).unwrap();
4236
                trace!(
4237
                    "queue_fill_dispatch(): src#{:?}@+{}B ({}B) -> dst#{:?}@+{}B ({}B)",
×
4238
                    src_buffer.id(),
×
4239
                    src_binding_offset,
×
4240
                    src_binding_size.get(),
×
4241
                    dst_buffer.id(),
×
4242
                    dst_binding_offset,
4243
                    -1, //dst_binding_size.get(),
4244
                );
4245
                let src_offset = std::mem::offset_of!(GpuEffectMetadata, alive_count) as u32 / 4;
4246
                debug_assert_eq!(
4247
                    src_offset, 5,
4248
                    "GpuEffectMetadata changed, update this assert."
×
4249
                );
4250
                // FIXME - This is a quick fix to get 0.15 out. The previous code used the
4251
                // dynamic binding offset, but the indirect dispatch structs are only 12 bytes,
4252
                // so are not aligned to min_storage_buffer_offset_alignment. The fix uses a
4253
                // binding offset of 0 and binds the entire destination buffer,
4254
                // then use the dst_offset value embedded inside the GpuBufferOperationArgs to
4255
                // index the proper offset in the buffer. This requires of
4256
                // course binding the entire buffer, or at least enough to index all operations
4257
                // (hence the None below). This is not really a general solution, so should be
4258
                // reviewed.
4259
                let dst_offset = sort_bind_groups
×
4260
                    .get_indirect_dispatch_byte_offset(sort_fill_indirect_dispatch_index)
×
4261
                    / 4;
×
4262
                sort_queue.enqueue(
×
4263
                    GpuBufferOperationType::FillDispatchArgs,
×
4264
                    GpuBufferOperationArgs {
×
4265
                        src_offset,
×
4266
                        src_stride: effects_meta.gpu_limits.effect_metadata_aligned_size.get() / 4,
×
4267
                        dst_offset,
×
4268
                        dst_stride: GpuDispatchIndirect::SHADER_SIZE.get() as u32 / 4,
×
4269
                        count: 1,
×
4270
                    },
4271
                    src_buffer,
×
4272
                    src_binding_offset,
×
4273
                    Some(src_binding_size),
×
4274
                    dst_buffer,
×
4275
                    dst_binding_offset,
×
4276
                    None, //Some(dst_binding_size),
×
4277
                );
4278
            }
4279
        }
4280

4281
        let effect_batch_index = sorted_effect_batches.push(effect_batch);
×
4282
        trace!(
×
4283
            "Spawned effect batch #{:?} from cached instance on entity {:?}.",
×
4284
            effect_batch_index,
4285
            entity,
4286
        );
4287

4288
        // Spawn an EffectDrawBatch, to actually drive rendering.
4289
        commands
4290
            .spawn(EffectDrawBatch {
4291
                effect_batch_index,
4292
                translation,
4293
            })
4294
            .insert(TemporaryRenderEntity);
4295
    }
4296

4297
    debug_assert!(sorted_effect_batches.dispatch_queue_index.is_none());
×
4298
    if !sort_queue.operation_queue.is_empty() {
×
4299
        sorted_effect_batches.dispatch_queue_index = Some(gpu_buffer_operations.submit(sort_queue));
×
4300
    }
4301

4302
    sorted_effect_batches.sort();
×
4303
}
4304

4305
/// Per-buffer bind groups for a GPU effect buffer.
4306
///
4307
/// This contains all bind groups specific to a single [`EffectBuffer`].
4308
///
4309
/// [`EffectBuffer`]: crate::render::effect_cache::EffectBuffer
4310
pub(crate) struct BufferBindGroups {
4311
    /// Bind group for the render shader.
4312
    ///
4313
    /// ```wgsl
4314
    /// @binding(0) var<storage, read> particle_buffer : ParticleBuffer;
4315
    /// @binding(1) var<storage, read> indirect_buffer : IndirectBuffer;
4316
    /// @binding(2) var<storage, read> spawner : Spawner;
4317
    /// ```
4318
    render: BindGroup,
4319
    // /// Bind group for filling the indirect dispatch arguments of any child init
4320
    // /// pass.
4321
    // ///
4322
    // /// This bind group is optional; it's only created if the current effect has
4323
    // /// a GPU spawn event buffer, irrelevant of whether it has child effects
4324
    // /// (although normally the event buffer is not created if there's no
4325
    // /// children).
4326
    // ///
4327
    // /// The source buffer is always the current effect's event buffer. The
4328
    // /// destination buffer is the global shared buffer for indirect fill args
4329
    // /// operations owned by the [`EffectCache`]. The uniform buffer of operation
4330
    // /// args contains the data to index the relevant part of the global shared
4331
    // /// buffer for this effect buffer; it may contain multiple entries in case
4332
    // /// multiple effects are batched inside the current effect buffer.
4333
    // ///
4334
    // /// ```wgsl
4335
    // /// @group(0) @binding(0) var<uniform> args : BufferOperationArgs;
4336
    // /// @group(0) @binding(1) var<storage, read> src_buffer : array<u32>;
4337
    // /// @group(0) @binding(2) var<storage, read_write> dst_buffer : array<u32>;
4338
    // /// ```
4339
    // init_fill_dispatch: Option<BindGroup>,
4340
}
4341

4342
/// Combination of a texture layout and the bound textures.
4343
#[derive(Debug, Default, Clone, PartialEq, Eq, Hash)]
4344
struct Material {
4345
    layout: TextureLayout,
4346
    textures: Vec<AssetId<Image>>,
4347
}
4348

4349
impl Material {
4350
    /// Get the bind group entries to create a bind group.
4351
    pub fn make_entries<'a>(
×
4352
        &self,
4353
        gpu_images: &'a RenderAssets<GpuImage>,
4354
    ) -> Result<Vec<BindGroupEntry<'a>>, ()> {
4355
        if self.textures.is_empty() {
×
4356
            return Ok(vec![]);
×
4357
        }
4358

4359
        let entries: Vec<BindGroupEntry<'a>> = self
×
4360
            .textures
×
4361
            .iter()
4362
            .enumerate()
4363
            .flat_map(|(index, id)| {
×
4364
                let base_binding = index as u32 * 2;
×
4365
                if let Some(gpu_image) = gpu_images.get(*id) {
×
4366
                    vec![
×
4367
                        BindGroupEntry {
×
4368
                            binding: base_binding,
×
4369
                            resource: BindingResource::TextureView(&gpu_image.texture_view),
×
4370
                        },
4371
                        BindGroupEntry {
×
4372
                            binding: base_binding + 1,
×
4373
                            resource: BindingResource::Sampler(&gpu_image.sampler),
×
4374
                        },
4375
                    ]
4376
                } else {
4377
                    vec![]
×
4378
                }
4379
            })
4380
            .collect();
4381
        if entries.len() == self.textures.len() * 2 {
×
4382
            return Ok(entries);
×
4383
        }
4384
        Err(())
×
4385
    }
4386
}
4387

4388
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
4389
struct BindingKey {
4390
    pub buffer_id: BufferId,
4391
    pub offset: u32,
4392
    pub size: NonZeroU32,
4393
}
4394

4395
impl<'a> From<BufferSlice<'a>> for BindingKey {
4396
    fn from(value: BufferSlice<'a>) -> Self {
×
4397
        Self {
4398
            buffer_id: value.buffer.id(),
×
4399
            offset: value.offset,
×
4400
            size: value.size,
×
4401
        }
4402
    }
4403
}
4404

4405
impl<'a> From<&BufferSlice<'a>> for BindingKey {
4406
    fn from(value: &BufferSlice<'a>) -> Self {
×
4407
        Self {
4408
            buffer_id: value.buffer.id(),
×
4409
            offset: value.offset,
×
4410
            size: value.size,
×
4411
        }
4412
    }
4413
}
4414

4415
impl From<&BufferBindingSource> for BindingKey {
4416
    fn from(value: &BufferBindingSource) -> Self {
×
4417
        Self {
4418
            buffer_id: value.buffer.id(),
×
4419
            offset: value.offset,
×
4420
            size: value.size,
×
4421
        }
4422
    }
4423
}
4424

4425
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
4426
struct ConsumeEventKey {
4427
    child_infos_buffer_id: BufferId,
4428
    events: BindingKey,
4429
}
4430

4431
impl From<&ConsumeEventBuffers<'_>> for ConsumeEventKey {
4432
    fn from(value: &ConsumeEventBuffers) -> Self {
×
4433
        Self {
4434
            child_infos_buffer_id: value.child_infos_buffer.id(),
×
4435
            events: value.events.into(),
×
4436
        }
4437
    }
4438
}
4439

4440
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
4441
struct InitMetadataBindGroupKey {
4442
    pub buffer_index: u32,
4443
    pub effect_metadata_buffer: BufferId,
4444
    pub effect_metadata_offset: u32,
4445
    pub consume_event_key: Option<ConsumeEventKey>,
4446
}
4447

4448
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
4449
struct UpdateMetadataBindGroupKey {
4450
    pub buffer_index: u32,
4451
    pub effect_metadata_buffer: BufferId,
4452
    pub effect_metadata_offset: u32,
4453
    pub child_info_buffer_id: Option<BufferId>,
4454
    pub event_buffers_keys: Vec<BindingKey>,
4455
}
4456

4457
struct CachedBindGroup<K: Eq> {
4458
    /// Key the bind group was created from. Each time the key changes, the bind
4459
    /// group should be re-created.
4460
    key: K,
4461
    /// Bind group created from the key.
4462
    bind_group: BindGroup,
4463
}
4464

4465
#[derive(Debug, Clone, Copy)]
4466
struct BufferSlice<'a> {
4467
    pub buffer: &'a Buffer,
4468
    pub offset: u32,
4469
    pub size: NonZeroU32,
4470
}
4471

4472
impl<'a> From<BufferSlice<'a>> for BufferBinding<'a> {
4473
    fn from(value: BufferSlice<'a>) -> Self {
×
4474
        Self {
4475
            buffer: value.buffer,
×
4476
            offset: value.offset.into(),
×
4477
            size: Some(value.size.into()),
×
4478
        }
4479
    }
4480
}
4481

4482
impl<'a> From<&BufferSlice<'a>> for BufferBinding<'a> {
4483
    fn from(value: &BufferSlice<'a>) -> Self {
×
4484
        Self {
4485
            buffer: value.buffer,
×
4486
            offset: value.offset.into(),
×
4487
            size: Some(value.size.into()),
×
4488
        }
4489
    }
4490
}
4491

4492
impl<'a> From<&'a BufferBindingSource> for BufferSlice<'a> {
4493
    fn from(value: &'a BufferBindingSource) -> Self {
×
4494
        Self {
4495
            buffer: &value.buffer,
×
4496
            offset: value.offset,
×
4497
            size: value.size,
×
4498
        }
4499
    }
4500
}
4501

4502
/// Optional input to [`EffectBindGroups::get_or_create_init_metadata()`] when
4503
/// the init pass consumes GPU events as a mechanism to spawn particles.
4504
struct ConsumeEventBuffers<'a> {
4505
    /// Entire buffer containing the [`GpuChildInfo`] entries for all effects.
4506
    /// This is dynamically indexed inside the shader.
4507
    child_infos_buffer: &'a Buffer,
4508
    /// Slice of the [`EventBuffer`] where the GPU spawn events are stored.
4509
    events: BufferSlice<'a>,
4510
}
4511

4512
#[derive(Default, Resource)]
4513
pub struct EffectBindGroups {
4514
    /// Map from buffer index to the bind groups shared among all effects that
4515
    /// use that buffer.
4516
    particle_buffers: HashMap<u32, BufferBindGroups>,
4517
    /// Map of bind groups for image assets used as particle textures.
4518
    images: HashMap<AssetId<Image>, BindGroup>,
4519
    /// Map from buffer index to its metadata bind group (group 3) for the init
4520
    /// pass.
4521
    // FIXME - doesn't work with batching; this should be the instance ID
4522
    init_metadata_bind_groups: HashMap<u32, CachedBindGroup<InitMetadataBindGroupKey>>,
4523
    /// Map from buffer index to its metadata bind group (group 3) for the
4524
    /// update pass.
4525
    // FIXME - doesn't work with batching; this should be the instance ID
4526
    update_metadata_bind_groups: HashMap<u32, CachedBindGroup<UpdateMetadataBindGroupKey>>,
4527
    /// Map from an effect material to its bind group.
4528
    material_bind_groups: HashMap<Material, BindGroup>,
4529
}
4530

4531
impl EffectBindGroups {
4532
    pub fn particle_render(&self, buffer_index: u32) -> Option<&BindGroup> {
×
4533
        self.particle_buffers
×
4534
            .get(&buffer_index)
×
4535
            .map(|bg| &bg.render)
×
4536
    }
4537

4538
    /// Retrieve the metadata@3 bind group for the init pass, creating it if
4539
    /// needed.
4540
    pub(self) fn get_or_create_init_metadata(
×
4541
        &mut self,
4542
        effect_batch: &EffectBatch,
4543
        gpu_limits: &GpuLimits,
4544
        render_device: &RenderDevice,
4545
        layout: &BindGroupLayout,
4546
        effect_metadata_buffer: &Buffer,
4547
        consume_event_buffers: Option<ConsumeEventBuffers>,
4548
    ) -> Result<&BindGroup, ()> {
4549
        let DispatchBufferIndices {
×
4550
            effect_metadata_buffer_table_id,
×
4551
            ..
×
4552
        } = &effect_batch.dispatch_buffer_indices;
×
4553

4554
        let effect_metadata_offset =
×
4555
            gpu_limits.effect_metadata_offset(effect_metadata_buffer_table_id.0) as u32;
×
4556
        let key = InitMetadataBindGroupKey {
4557
            buffer_index: effect_batch.buffer_index,
×
4558
            effect_metadata_buffer: effect_metadata_buffer.id(),
×
4559
            effect_metadata_offset,
4560
            consume_event_key: consume_event_buffers.as_ref().map(Into::into),
×
4561
        };
4562

4563
        let make_entry = || {
×
4564
            let mut entries = Vec::with_capacity(3);
×
4565
            entries.push(
×
4566
                // @group(3) @binding(0) var<storage, read_write> effect_metadata : EffectMetadata;
4567
                BindGroupEntry {
×
4568
                    binding: 0,
×
4569
                    resource: BindingResource::Buffer(BufferBinding {
×
4570
                        buffer: effect_metadata_buffer,
×
4571
                        offset: key.effect_metadata_offset as u64,
×
4572
                        size: Some(gpu_limits.effect_metadata_size()),
×
4573
                    }),
4574
                },
4575
            );
4576
            if let Some(consume_event_buffers) = consume_event_buffers.as_ref() {
×
4577
                entries.push(
4578
                    // @group(3) @binding(1) var<storage, read> child_info_buffer :
4579
                    // ChildInfoBuffer;
4580
                    BindGroupEntry {
4581
                        binding: 1,
4582
                        resource: BindingResource::Buffer(BufferBinding {
4583
                            buffer: consume_event_buffers.child_infos_buffer,
4584
                            offset: 0,
4585
                            size: None,
4586
                        }),
4587
                    },
4588
                );
4589
                entries.push(
4590
                    // @group(3) @binding(2) var<storage, read> event_buffer : EventBuffer;
4591
                    BindGroupEntry {
4592
                        binding: 2,
4593
                        resource: BindingResource::Buffer(consume_event_buffers.events.into()),
4594
                    },
4595
                );
4596
            }
4597

4598
            let bind_group = render_device.create_bind_group(
×
4599
                "hanabi:bind_group:init:metadata@3",
4600
                layout,
×
4601
                &entries[..],
×
4602
            );
4603

4604
            trace!(
×
4605
                    "Created new metadata@3 bind group for init pass and buffer index {}: effect_metadata=#{}",
×
4606
                    effect_batch.buffer_index,
4607
                    effect_metadata_buffer_table_id.0,
4608
                );
4609

4610
            bind_group
×
4611
        };
4612

4613
        Ok(&self
×
4614
            .init_metadata_bind_groups
×
4615
            .entry(effect_batch.buffer_index)
×
4616
            .and_modify(|cbg| {
×
4617
                if cbg.key != key {
×
4618
                    trace!(
×
4619
                        "Bind group key changed for init metadata@3, re-creating bind group... old={:?} new={:?}",
×
4620
                        cbg.key,
4621
                        key
4622
                    );
4623
                    cbg.key = key;
×
4624
                    cbg.bind_group = make_entry();
×
4625
                }
4626
            })
4627
            .or_insert_with(|| {
×
4628
                trace!("Inserting new bind group for init metadata@3 with key={:?}", key);
×
4629
                CachedBindGroup {
×
4630
                    key,
×
4631
                    bind_group: make_entry(),
×
4632
                }
4633
            })
4634
            .bind_group)
×
4635
    }
4636

4637
    /// Retrieve the metadata@3 bind group for the update pass, creating it if
4638
    /// needed.
4639
    pub(self) fn get_or_create_update_metadata(
×
4640
        &mut self,
4641
        effect_batch: &EffectBatch,
4642
        gpu_limits: &GpuLimits,
4643
        render_device: &RenderDevice,
4644
        layout: &BindGroupLayout,
4645
        effect_metadata_buffer: &Buffer,
4646
        child_info_buffer: Option<&Buffer>,
4647
        event_buffers: &[(Entity, BufferBindingSource)],
4648
    ) -> Result<&BindGroup, ()> {
4649
        let DispatchBufferIndices {
×
4650
            effect_metadata_buffer_table_id,
×
4651
            ..
×
4652
        } = &effect_batch.dispatch_buffer_indices;
×
4653

4654
        // Check arguments consistency
4655
        assert_eq!(effect_batch.child_event_buffers.len(), event_buffers.len());
×
4656
        let emits_gpu_spawn_events = !event_buffers.is_empty();
×
4657
        let child_info_buffer_id = if emits_gpu_spawn_events {
×
4658
            child_info_buffer.as_ref().map(|buffer| buffer.id())
×
4659
        } else {
4660
            // Note: child_info_buffer can be Some() if allocated, but we only consider it
4661
            // if relevant, that is if the effect emits GPU spawn events.
4662
            None
×
4663
        };
4664
        assert_eq!(emits_gpu_spawn_events, child_info_buffer_id.is_some());
×
4665

4666
        let event_buffers_keys = event_buffers
×
4667
            .iter()
4668
            .map(|(_, buffer_binding_source)| buffer_binding_source.into())
×
4669
            .collect::<Vec<_>>();
4670

4671
        let key = UpdateMetadataBindGroupKey {
4672
            buffer_index: effect_batch.buffer_index,
×
4673
            effect_metadata_buffer: effect_metadata_buffer.id(),
×
4674
            effect_metadata_offset: gpu_limits
×
4675
                .effect_metadata_offset(effect_metadata_buffer_table_id.0)
4676
                as u32,
4677
            child_info_buffer_id,
4678
            event_buffers_keys,
4679
        };
4680

4681
        let make_entry = || {
×
4682
            let mut entries = Vec::with_capacity(2 + event_buffers.len());
×
4683
            // @group(3) @binding(0) var<storage, read_write> effect_metadata :
4684
            // EffectMetadata;
4685
            entries.push(BindGroupEntry {
×
4686
                binding: 0,
×
4687
                resource: BindingResource::Buffer(BufferBinding {
×
4688
                    buffer: effect_metadata_buffer,
×
4689
                    offset: key.effect_metadata_offset as u64,
×
4690
                    size: Some(gpu_limits.effect_metadata_aligned_size.into()),
×
4691
                }),
4692
            });
4693
            if emits_gpu_spawn_events {
×
4694
                let child_info_buffer = child_info_buffer.unwrap();
×
4695

4696
                // @group(3) @binding(1) var<storage, read_write> child_info_buffer :
4697
                // ChildInfoBuffer;
4698
                entries.push(BindGroupEntry {
×
4699
                    binding: 1,
×
4700
                    resource: BindingResource::Buffer(BufferBinding {
×
4701
                        buffer: child_info_buffer,
×
4702
                        offset: 0,
×
4703
                        size: None,
×
4704
                    }),
4705
                });
4706

4707
                for (index, (_, buffer_binding_source)) in event_buffers.iter().enumerate() {
×
4708
                    // @group(3) @binding(2+N) var<storage, read_write> event_buffer_N :
4709
                    // EventBuffer;
4710
                    // FIXME - BufferBindingSource originally was for Events, counting in u32, but
4711
                    // then moved to counting in bytes, so now need some conversion. Need to review
4712
                    // all of this...
4713
                    let mut buffer_binding: BufferBinding = buffer_binding_source.into();
4714
                    buffer_binding.offset *= 4;
4715
                    buffer_binding.size = buffer_binding
4716
                        .size
4717
                        .map(|sz| NonZeroU64::new(sz.get() * 4).unwrap());
×
4718
                    entries.push(BindGroupEntry {
4719
                        binding: 2 + index as u32,
4720
                        resource: BindingResource::Buffer(buffer_binding),
4721
                    });
4722
                }
4723
            }
4724

4725
            let bind_group = render_device.create_bind_group(
×
4726
                "hanabi:bind_group:update:metadata@3",
4727
                layout,
×
4728
                &entries[..],
×
4729
            );
4730

4731
            trace!(
×
4732
                "Created new metadata@3 bind group for update pass and buffer index {}: effect_metadata={}",
×
4733
                effect_batch.buffer_index,
4734
                effect_metadata_buffer_table_id.0,
4735
            );
4736

4737
            bind_group
×
4738
        };
4739

4740
        Ok(&self
×
4741
            .update_metadata_bind_groups
×
4742
            .entry(effect_batch.buffer_index)
×
4743
            .and_modify(|cbg| {
×
4744
                if cbg.key != key {
×
4745
                    trace!(
×
4746
                        "Bind group key changed for update metadata@3, re-creating bind group... old={:?} new={:?}",
×
4747
                        cbg.key,
4748
                        key
4749
                    );
4750
                    cbg.key = key.clone();
×
4751
                    cbg.bind_group = make_entry();
×
4752
                }
4753
            })
4754
            .or_insert_with(|| {
×
4755
                trace!(
×
4756
                    "Inserting new bind group for update metadata@3 with key={:?}",
×
4757
                    key
4758
                );
4759
                CachedBindGroup {
×
4760
                    key: key.clone(),
×
4761
                    bind_group: make_entry(),
×
4762
                }
4763
            })
4764
            .bind_group)
×
4765
    }
4766
}
4767

4768
#[derive(SystemParam)]
4769
pub struct QueueEffectsReadOnlyParams<'w, 's> {
4770
    #[cfg(feature = "2d")]
4771
    draw_functions_2d: Res<'w, DrawFunctions<Transparent2d>>,
4772
    #[cfg(feature = "3d")]
4773
    draw_functions_3d: Res<'w, DrawFunctions<Transparent3d>>,
4774
    #[cfg(feature = "3d")]
4775
    draw_functions_alpha_mask: Res<'w, DrawFunctions<AlphaMask3d>>,
4776
    #[cfg(feature = "3d")]
4777
    draw_functions_opaque: Res<'w, DrawFunctions<Opaque3d>>,
4778
    #[system_param(ignore)]
4779
    marker: PhantomData<&'s usize>,
4780
}
4781

4782
fn emit_sorted_draw<T, F>(
×
4783
    views: &Query<(Entity, &RenderVisibleEntities, &ExtractedView, &Msaa)>,
4784
    render_phases: &mut ResMut<ViewSortedRenderPhases<T>>,
4785
    view_entities: &mut FixedBitSet,
4786
    sorted_effect_batches: &SortedEffectBatches,
4787
    effect_draw_batches: &Query<(Entity, &mut EffectDrawBatch)>,
4788
    render_pipeline: &mut ParticlesRenderPipeline,
4789
    mut specialized_render_pipelines: Mut<SpecializedRenderPipelines<ParticlesRenderPipeline>>,
4790
    render_meshes: &RenderAssets<RenderMesh>,
4791
    pipeline_cache: &PipelineCache,
4792
    make_phase_item: F,
4793
    #[cfg(all(feature = "2d", feature = "3d"))] pipeline_mode: PipelineMode,
4794
) where
4795
    T: SortedPhaseItem,
4796
    F: Fn(CachedRenderPipelineId, (Entity, MainEntity), &EffectDrawBatch, &ExtractedView) -> T,
4797
{
4798
    trace!("emit_sorted_draw() {} views", views.iter().len());
×
4799

4800
    for (view_entity, visible_entities, view, msaa) in views.iter() {
×
4801
        trace!(
×
4802
            "Process new sorted view with {} visible particle effect entities",
×
4803
            visible_entities.len::<WithCompiledParticleEffect>()
×
4804
        );
4805

4806
        let Some(render_phase) = render_phases.get_mut(&view_entity) else {
×
4807
            continue;
×
4808
        };
4809

4810
        {
4811
            #[cfg(feature = "trace")]
4812
            let _span = bevy::utils::tracing::info_span!("collect_view_entities").entered();
×
4813

4814
            view_entities.clear();
×
4815
            view_entities.extend(
×
4816
                visible_entities
×
4817
                    .iter::<WithCompiledParticleEffect>()
×
4818
                    .map(|e| e.1.index() as usize),
×
4819
            );
4820
        }
4821

4822
        // For each view, loop over all the effect batches to determine if the effect
4823
        // needs to be rendered for that view, and enqueue a view-dependent
4824
        // batch if so.
4825
        for (draw_entity, draw_batch) in effect_draw_batches.iter() {
×
4826
            #[cfg(feature = "trace")]
4827
            let _span_draw = bevy::utils::tracing::info_span!("draw_batch").entered();
×
4828

4829
            trace!(
×
4830
                "Process draw batch: draw_entity={:?} effect_batch_index={:?}",
×
4831
                draw_entity,
×
4832
                draw_batch.effect_batch_index,
×
4833
            );
4834

4835
            // Get the EffectBatches this EffectDrawBatch is part of.
4836
            let Some(effect_batch) = sorted_effect_batches.get(draw_batch.effect_batch_index)
×
4837
            else {
×
4838
                continue;
×
4839
            };
4840

4841
            trace!(
×
4842
                "-> EffectBach: buffer_index={} spawner_base={} layout_flags={:?}",
×
4843
                effect_batch.buffer_index,
×
4844
                effect_batch.spawner_base,
×
4845
                effect_batch.layout_flags,
×
4846
            );
4847

4848
            // AlphaMask is a binned draw, so no sorted draw can possibly use it
4849
            if effect_batch
×
4850
                .layout_flags
×
4851
                .intersects(LayoutFlags::USE_ALPHA_MASK | LayoutFlags::OPAQUE)
×
4852
            {
4853
                trace!("Non-transparent batch. Skipped.");
×
4854
                continue;
×
4855
            }
4856

4857
            // Check if batch contains any entity visible in the current view. Otherwise we
4858
            // can skip the entire batch. Note: This is O(n^2) but (unlike
4859
            // the Sprite renderer this is inspired from) we don't expect more than
4860
            // a handful of particle effect instances, so would rather not pay the memory
4861
            // cost of a FixedBitSet for the sake of an arguable speed-up.
4862
            // TODO - Profile to confirm.
4863
            #[cfg(feature = "trace")]
4864
            let _span_check_vis = bevy::utils::tracing::info_span!("check_visibility").entered();
×
4865
            let has_visible_entity = effect_batch
×
4866
                .entities
×
4867
                .iter()
4868
                .any(|index| view_entities.contains(*index as usize));
×
4869
            if !has_visible_entity {
×
4870
                trace!("No visible entity for view, not emitting any draw call.");
×
4871
                continue;
×
4872
            }
4873
            #[cfg(feature = "trace")]
4874
            _span_check_vis.exit();
×
4875

4876
            // Create and cache the bind group layout for this texture layout
4877
            render_pipeline.cache_material(&effect_batch.texture_layout);
×
4878

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

4882
            let local_space_simulation = effect_batch
×
4883
                .layout_flags
×
4884
                .contains(LayoutFlags::LOCAL_SPACE_SIMULATION);
×
4885
            let alpha_mask = ParticleRenderAlphaMaskPipelineKey::from(effect_batch.layout_flags);
×
4886
            let flipbook = effect_batch.layout_flags.contains(LayoutFlags::FLIPBOOK);
×
4887
            let needs_uv = effect_batch.layout_flags.contains(LayoutFlags::NEEDS_UV);
×
4888
            let needs_normal = effect_batch
×
4889
                .layout_flags
×
4890
                .contains(LayoutFlags::NEEDS_NORMAL);
×
4891
            let ribbons = effect_batch.layout_flags.contains(LayoutFlags::RIBBONS);
×
4892
            let image_count = effect_batch.texture_layout.layout.len() as u8;
×
4893

4894
            // FIXME - Maybe it's better to copy the mesh layout into the batch, instead of
4895
            // re-querying here...?
4896
            let Some(render_mesh) = render_meshes.get(effect_batch.mesh) else {
×
4897
                trace!("Batch has no render mesh, skipped.");
×
4898
                continue;
×
4899
            };
4900
            let mesh_layout = render_mesh.layout.clone();
×
4901

4902
            // Specialize the render pipeline based on the effect batch
4903
            trace!(
×
4904
                "Specializing render pipeline: render_shader={:?} image_count={} alpha_mask={:?} flipbook={:?} hdr={}",
×
4905
                effect_batch.render_shader,
×
4906
                image_count,
×
4907
                alpha_mask,
×
4908
                flipbook,
×
4909
                view.hdr
×
4910
            );
4911

4912
            // Add a draw pass for the effect batch
4913
            trace!("Emitting individual draw for batch");
×
4914

4915
            let alpha_mode = effect_batch.alpha_mode;
×
4916

4917
            #[cfg(feature = "trace")]
4918
            let _span_specialize = bevy::utils::tracing::info_span!("specialize").entered();
×
4919
            let render_pipeline_id = specialized_render_pipelines.specialize(
×
4920
                pipeline_cache,
×
4921
                render_pipeline,
×
4922
                ParticleRenderPipelineKey {
×
4923
                    shader: effect_batch.render_shader.clone(),
×
4924
                    mesh_layout: Some(mesh_layout),
×
4925
                    particle_layout: effect_batch.particle_layout.clone(),
×
4926
                    texture_layout: effect_batch.texture_layout.clone(),
×
4927
                    local_space_simulation,
×
4928
                    alpha_mask,
×
4929
                    alpha_mode,
×
4930
                    flipbook,
×
4931
                    needs_uv,
×
4932
                    needs_normal,
×
4933
                    ribbons,
×
4934
                    #[cfg(all(feature = "2d", feature = "3d"))]
×
4935
                    pipeline_mode,
×
4936
                    msaa_samples: msaa.samples(),
×
4937
                    hdr: view.hdr,
×
4938
                },
4939
            );
4940
            #[cfg(feature = "trace")]
4941
            _span_specialize.exit();
×
4942

4943
            trace!("+ Render pipeline specialized: id={:?}", render_pipeline_id,);
×
4944
            trace!(
×
4945
                "+ Add Transparent for batch on draw_entity {:?}: buffer_index={} \
×
4946
                spawner_base={} handle={:?}",
×
4947
                draw_entity,
×
4948
                effect_batch.buffer_index,
×
4949
                effect_batch.spawner_base,
×
4950
                effect_batch.handle
×
4951
            );
4952
            render_phase.add(make_phase_item(
×
4953
                render_pipeline_id,
×
4954
                (draw_entity, MainEntity::from(Entity::PLACEHOLDER)),
×
4955
                draw_batch,
×
4956
                view,
×
4957
            ));
4958
        }
4959
    }
4960
}
4961

4962
#[cfg(feature = "3d")]
4963
fn emit_binned_draw<T, F>(
×
4964
    views: &Query<(Entity, &RenderVisibleEntities, &ExtractedView, &Msaa)>,
4965
    render_phases: &mut ResMut<ViewBinnedRenderPhases<T>>,
4966
    view_entities: &mut FixedBitSet,
4967
    sorted_effect_batches: &SortedEffectBatches,
4968
    effect_draw_batches: &Query<(Entity, &mut EffectDrawBatch)>,
4969
    render_pipeline: &mut ParticlesRenderPipeline,
4970
    mut specialized_render_pipelines: Mut<SpecializedRenderPipelines<ParticlesRenderPipeline>>,
4971
    pipeline_cache: &PipelineCache,
4972
    render_meshes: &RenderAssets<RenderMesh>,
4973
    make_bin_key: F,
4974
    #[cfg(all(feature = "2d", feature = "3d"))] pipeline_mode: PipelineMode,
4975
    alpha_mask: ParticleRenderAlphaMaskPipelineKey,
4976
) where
4977
    T: BinnedPhaseItem,
4978
    F: Fn(CachedRenderPipelineId, &EffectDrawBatch, &ExtractedView) -> T::BinKey,
4979
{
4980
    use bevy::render::render_phase::BinnedRenderPhaseType;
4981

4982
    trace!("emit_binned_draw() {} views", views.iter().len());
×
4983

4984
    for (view_entity, visible_entities, view, msaa) in views.iter() {
×
4985
        trace!("Process new binned view (alpha_mask={:?})", alpha_mask);
×
4986

4987
        let Some(render_phase) = render_phases.get_mut(&view_entity) else {
×
4988
            continue;
×
4989
        };
4990

4991
        {
4992
            #[cfg(feature = "trace")]
4993
            let _span = bevy::utils::tracing::info_span!("collect_view_entities").entered();
×
4994

4995
            view_entities.clear();
×
4996
            view_entities.extend(
×
4997
                visible_entities
×
4998
                    .iter::<WithCompiledParticleEffect>()
×
4999
                    .map(|e| e.1.index() as usize),
×
5000
            );
5001
        }
5002

5003
        // For each view, loop over all the effect batches to determine if the effect
5004
        // needs to be rendered for that view, and enqueue a view-dependent
5005
        // batch if so.
5006
        for (draw_entity, draw_batch) in effect_draw_batches.iter() {
×
5007
            #[cfg(feature = "trace")]
5008
            let _span_draw = bevy::utils::tracing::info_span!("draw_batch").entered();
×
5009

5010
            trace!(
×
5011
                "Process draw batch: draw_entity={:?} effect_batch_index={:?}",
×
5012
                draw_entity,
×
5013
                draw_batch.effect_batch_index,
×
5014
            );
5015

5016
            // Get the EffectBatches this EffectDrawBatch is part of.
5017
            let Some(effect_batch) = sorted_effect_batches.get(draw_batch.effect_batch_index)
×
5018
            else {
×
5019
                continue;
×
5020
            };
5021

5022
            trace!(
×
5023
                "-> EffectBaches: buffer_index={} spawner_base={} layout_flags={:?}",
×
5024
                effect_batch.buffer_index,
×
5025
                effect_batch.spawner_base,
×
5026
                effect_batch.layout_flags,
×
5027
            );
5028

5029
            if ParticleRenderAlphaMaskPipelineKey::from(effect_batch.layout_flags) != alpha_mask {
×
5030
                trace!(
×
5031
                    "Mismatching alpha mask pipeline key (batches={:?}, expected={:?}). Skipped.",
×
5032
                    effect_batch.layout_flags,
×
5033
                    alpha_mask
×
5034
                );
5035
                continue;
×
5036
            }
5037

5038
            // Check if batch contains any entity visible in the current view. Otherwise we
5039
            // can skip the entire batch. Note: This is O(n^2) but (unlike
5040
            // the Sprite renderer this is inspired from) we don't expect more than
5041
            // a handful of particle effect instances, so would rather not pay the memory
5042
            // cost of a FixedBitSet for the sake of an arguable speed-up.
5043
            // TODO - Profile to confirm.
5044
            #[cfg(feature = "trace")]
5045
            let _span_check_vis = bevy::utils::tracing::info_span!("check_visibility").entered();
×
5046
            let has_visible_entity = effect_batch
×
5047
                .entities
×
5048
                .iter()
5049
                .any(|index| view_entities.contains(*index as usize));
×
5050
            if !has_visible_entity {
×
5051
                trace!("No visible entity for view, not emitting any draw call.");
×
5052
                continue;
×
5053
            }
5054
            #[cfg(feature = "trace")]
5055
            _span_check_vis.exit();
×
5056

5057
            // Create and cache the bind group layout for this texture layout
5058
            render_pipeline.cache_material(&effect_batch.texture_layout);
×
5059

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

5063
            let local_space_simulation = effect_batch
×
5064
                .layout_flags
×
5065
                .contains(LayoutFlags::LOCAL_SPACE_SIMULATION);
×
5066
            let alpha_mask = ParticleRenderAlphaMaskPipelineKey::from(effect_batch.layout_flags);
×
5067
            let flipbook = effect_batch.layout_flags.contains(LayoutFlags::FLIPBOOK);
×
5068
            let needs_uv = effect_batch.layout_flags.contains(LayoutFlags::NEEDS_UV);
×
5069
            let needs_normal = effect_batch
×
5070
                .layout_flags
×
5071
                .contains(LayoutFlags::NEEDS_NORMAL);
×
5072
            let ribbons = effect_batch.layout_flags.contains(LayoutFlags::RIBBONS);
×
5073
            let image_count = effect_batch.texture_layout.layout.len() as u8;
×
5074
            let render_mesh = render_meshes.get(effect_batch.mesh);
×
5075

5076
            // Specialize the render pipeline based on the effect batch
5077
            trace!(
×
5078
                "Specializing render pipeline: render_shaders={:?} image_count={} alpha_mask={:?} flipbook={:?} hdr={}",
×
5079
                effect_batch.render_shader,
×
5080
                image_count,
×
5081
                alpha_mask,
×
5082
                flipbook,
×
5083
                view.hdr
×
5084
            );
5085

5086
            // Add a draw pass for the effect batch
5087
            trace!("Emitting individual draw for batch");
×
5088

5089
            let alpha_mode = effect_batch.alpha_mode;
×
5090

5091
            let Some(mesh_layout) = render_mesh.map(|gpu_mesh| gpu_mesh.layout.clone()) else {
×
5092
                trace!("Missing mesh vertex buffer layout. Skipped.");
×
5093
                continue;
×
5094
            };
5095

5096
            #[cfg(feature = "trace")]
5097
            let _span_specialize = bevy::utils::tracing::info_span!("specialize").entered();
×
5098
            let render_pipeline_id = specialized_render_pipelines.specialize(
×
5099
                pipeline_cache,
×
5100
                render_pipeline,
×
5101
                ParticleRenderPipelineKey {
×
5102
                    shader: effect_batch.render_shader.clone(),
×
5103
                    mesh_layout: Some(mesh_layout),
×
5104
                    particle_layout: effect_batch.particle_layout.clone(),
×
5105
                    texture_layout: effect_batch.texture_layout.clone(),
×
5106
                    local_space_simulation,
×
5107
                    alpha_mask,
×
5108
                    alpha_mode,
×
5109
                    flipbook,
×
5110
                    needs_uv,
×
5111
                    needs_normal,
×
5112
                    ribbons,
×
5113
                    #[cfg(all(feature = "2d", feature = "3d"))]
×
5114
                    pipeline_mode,
×
5115
                    msaa_samples: msaa.samples(),
×
5116
                    hdr: view.hdr,
×
5117
                },
5118
            );
5119
            #[cfg(feature = "trace")]
5120
            _span_specialize.exit();
×
5121

5122
            trace!("+ Render pipeline specialized: id={:?}", render_pipeline_id,);
×
5123
            trace!(
×
5124
                "+ Add Transparent for batch on draw_entity {:?}: buffer_index={} \
×
5125
                spawner_base={} handle={:?}",
×
5126
                draw_entity,
×
5127
                effect_batch.buffer_index,
×
5128
                effect_batch.spawner_base,
×
5129
                effect_batch.handle
×
5130
            );
5131
            render_phase.add(
×
5132
                make_bin_key(render_pipeline_id, draw_batch, view),
×
5133
                (draw_entity, MainEntity::from(Entity::PLACEHOLDER)),
×
5134
                BinnedRenderPhaseType::NonMesh,
×
5135
            );
5136
        }
5137
    }
5138
}
5139

5140
#[allow(clippy::too_many_arguments)]
5141
pub(crate) fn queue_effects(
×
5142
    views: Query<(Entity, &RenderVisibleEntities, &ExtractedView, &Msaa)>,
5143
    effects_meta: Res<EffectsMeta>,
5144
    mut render_pipeline: ResMut<ParticlesRenderPipeline>,
5145
    mut specialized_render_pipelines: ResMut<SpecializedRenderPipelines<ParticlesRenderPipeline>>,
5146
    pipeline_cache: Res<PipelineCache>,
5147
    mut effect_bind_groups: ResMut<EffectBindGroups>,
5148
    sorted_effect_batches: Res<SortedEffectBatches>,
5149
    effect_draw_batches: Query<(Entity, &mut EffectDrawBatch)>,
5150
    events: Res<EffectAssetEvents>,
5151
    render_meshes: Res<RenderAssets<RenderMesh>>,
5152
    read_params: QueueEffectsReadOnlyParams,
5153
    mut view_entities: Local<FixedBitSet>,
5154
    #[cfg(feature = "2d")] mut transparent_2d_render_phases: ResMut<
5155
        ViewSortedRenderPhases<Transparent2d>,
5156
    >,
5157
    #[cfg(feature = "3d")] mut transparent_3d_render_phases: ResMut<
5158
        ViewSortedRenderPhases<Transparent3d>,
5159
    >,
5160
    #[cfg(feature = "3d")] mut alpha_mask_3d_render_phases: ResMut<
5161
        ViewBinnedRenderPhases<AlphaMask3d>,
5162
    >,
5163
) {
5164
    #[cfg(feature = "trace")]
5165
    let _span = bevy::utils::tracing::info_span!("hanabi:queue_effects").entered();
×
5166

5167
    trace!("queue_effects");
×
5168

5169
    // If an image has changed, the GpuImage has (probably) changed
5170
    for event in &events.images {
×
5171
        match event {
5172
            AssetEvent::Added { .. } => None,
×
5173
            AssetEvent::LoadedWithDependencies { .. } => None,
×
5174
            AssetEvent::Unused { .. } => None,
×
5175
            AssetEvent::Modified { id } => {
×
5176
                trace!("Destroy bind group of modified image asset {:?}", id);
×
5177
                effect_bind_groups.images.remove(id)
×
5178
            }
5179
            AssetEvent::Removed { id } => {
×
5180
                trace!("Destroy bind group of removed image asset {:?}", id);
×
5181
                effect_bind_groups.images.remove(id)
×
5182
            }
5183
        };
5184
    }
5185

5186
    if effects_meta.spawner_buffer.buffer().is_none() || effects_meta.spawner_buffer.is_empty() {
×
5187
        // No spawners are active
5188
        return;
×
5189
    }
5190

5191
    // Loop over all 2D cameras/views that need to render effects
5192
    #[cfg(feature = "2d")]
5193
    {
5194
        #[cfg(feature = "trace")]
5195
        let _span_draw = bevy::utils::tracing::info_span!("draw_2d").entered();
5196

5197
        let draw_effects_function_2d = read_params
5198
            .draw_functions_2d
5199
            .read()
5200
            .get_id::<DrawEffects>()
5201
            .unwrap();
5202

5203
        // Effects with full alpha blending
5204
        if !views.is_empty() {
5205
            trace!("Emit effect draw calls for alpha blended 2D views...");
×
5206
            emit_sorted_draw(
5207
                &views,
5208
                &mut transparent_2d_render_phases,
5209
                &mut view_entities,
5210
                &sorted_effect_batches,
5211
                &effect_draw_batches,
5212
                &mut render_pipeline,
5213
                specialized_render_pipelines.reborrow(),
5214
                &render_meshes,
5215
                &pipeline_cache,
5216
                |id, entity, draw_batch, _view| Transparent2d {
×
5217
                    sort_key: FloatOrd(draw_batch.translation.z),
×
5218
                    entity,
×
5219
                    pipeline: id,
×
5220
                    draw_function: draw_effects_function_2d,
×
5221
                    batch_range: 0..1,
×
5222
                    extra_index: PhaseItemExtraIndex::NONE,
×
5223
                },
5224
                #[cfg(feature = "3d")]
5225
                PipelineMode::Camera2d,
5226
            );
5227
        }
5228
    }
5229

5230
    // Loop over all 3D cameras/views that need to render effects
5231
    #[cfg(feature = "3d")]
5232
    {
5233
        #[cfg(feature = "trace")]
5234
        let _span_draw = bevy::utils::tracing::info_span!("draw_3d").entered();
5235

5236
        // Effects with full alpha blending
5237
        if !views.is_empty() {
5238
            trace!("Emit effect draw calls for alpha blended 3D views...");
×
5239

5240
            let draw_effects_function_3d = read_params
5241
                .draw_functions_3d
5242
                .read()
5243
                .get_id::<DrawEffects>()
5244
                .unwrap();
5245

5246
            emit_sorted_draw(
5247
                &views,
5248
                &mut transparent_3d_render_phases,
5249
                &mut view_entities,
5250
                &sorted_effect_batches,
5251
                &effect_draw_batches,
5252
                &mut render_pipeline,
5253
                specialized_render_pipelines.reborrow(),
5254
                &render_meshes,
5255
                &pipeline_cache,
5256
                |id, entity, batch, view| Transparent3d {
×
5257
                    draw_function: draw_effects_function_3d,
×
5258
                    pipeline: id,
×
5259
                    entity,
×
5260
                    distance: view
×
5261
                        .rangefinder3d()
×
5262
                        .distance_translation(&batch.translation),
×
5263
                    batch_range: 0..1,
×
5264
                    extra_index: PhaseItemExtraIndex::NONE,
×
5265
                },
5266
                #[cfg(feature = "2d")]
5267
                PipelineMode::Camera3d,
5268
            );
5269
        }
5270

5271
        // Effects with alpha mask
5272
        if !views.is_empty() {
5273
            #[cfg(feature = "trace")]
5274
            let _span_draw = bevy::utils::tracing::info_span!("draw_alphamask").entered();
×
5275

5276
            trace!("Emit effect draw calls for alpha masked 3D views...");
×
5277

5278
            let draw_effects_function_alpha_mask = read_params
5279
                .draw_functions_alpha_mask
5280
                .read()
5281
                .get_id::<DrawEffects>()
5282
                .unwrap();
5283

5284
            emit_binned_draw(
5285
                &views,
5286
                &mut alpha_mask_3d_render_phases,
5287
                &mut view_entities,
5288
                &sorted_effect_batches,
5289
                &effect_draw_batches,
5290
                &mut render_pipeline,
5291
                specialized_render_pipelines.reborrow(),
5292
                &pipeline_cache,
5293
                &render_meshes,
5294
                |id, _batch, _view| OpaqueNoLightmap3dBinKey {
×
5295
                    pipeline: id,
×
5296
                    draw_function: draw_effects_function_alpha_mask,
×
5297
                    asset_id: AssetId::<Image>::default().untyped(),
×
5298
                    material_bind_group_id: None,
×
5299
                    // },
5300
                    // distance: view
5301
                    //     .rangefinder3d()
5302
                    //     .distance_translation(&batch.translation_3d),
5303
                    // batch_range: 0..1,
5304
                    // extra_index: PhaseItemExtraIndex::NONE,
5305
                },
5306
                #[cfg(feature = "2d")]
5307
                PipelineMode::Camera3d,
5308
                ParticleRenderAlphaMaskPipelineKey::AlphaMask,
5309
            );
5310
        }
5311

5312
        // Opaque particles
5313
        if !views.is_empty() {
5314
            #[cfg(feature = "trace")]
5315
            let _span_draw = bevy::utils::tracing::info_span!("draw_opaque").entered();
×
5316

5317
            trace!("Emit effect draw calls for opaque 3D views...");
×
5318

5319
            let draw_effects_function_opaque = read_params
5320
                .draw_functions_opaque
5321
                .read()
5322
                .get_id::<DrawEffects>()
5323
                .unwrap();
5324

5325
            emit_binned_draw(
5326
                &views,
5327
                &mut alpha_mask_3d_render_phases,
5328
                &mut view_entities,
5329
                &sorted_effect_batches,
5330
                &effect_draw_batches,
5331
                &mut render_pipeline,
5332
                specialized_render_pipelines.reborrow(),
5333
                &pipeline_cache,
5334
                &render_meshes,
5335
                |id, _batch, _view| OpaqueNoLightmap3dBinKey {
×
5336
                    pipeline: id,
×
5337
                    draw_function: draw_effects_function_opaque,
×
5338
                    asset_id: AssetId::<Image>::default().untyped(),
×
5339
                    material_bind_group_id: None,
×
5340
                    // },
5341
                    // distance: view
5342
                    //     .rangefinder3d()
5343
                    //     .distance_translation(&batch.translation_3d),
5344
                    // batch_range: 0..1,
5345
                    // extra_index: PhaseItemExtraIndex::NONE,
5346
                },
5347
                #[cfg(feature = "2d")]
5348
                PipelineMode::Camera3d,
5349
                ParticleRenderAlphaMaskPipelineKey::Opaque,
5350
            );
5351
        }
5352
    }
5353
}
5354

5355
/// Prepare GPU resources for effect rendering.
5356
///
5357
/// This system runs in the [`RenderSet::PrepareResources`] render set, after
5358
/// Bevy has updated the [`ViewUniforms`], which need to be referenced to get
5359
/// access to the current camera view.
5360
pub(crate) fn prepare_gpu_resources(
×
5361
    mut effects_meta: ResMut<EffectsMeta>,
5362
    //mut effect_cache: ResMut<EffectCache>,
5363
    mut event_cache: ResMut<EventCache>,
5364
    mut effect_bind_groups: ResMut<EffectBindGroups>,
5365
    mut sort_bind_groups: ResMut<SortBindGroups>,
5366
    render_device: Res<RenderDevice>,
5367
    render_queue: Res<RenderQueue>,
5368
    view_uniforms: Res<ViewUniforms>,
5369
    render_pipeline: Res<ParticlesRenderPipeline>,
5370
) {
5371
    // Get the binding for the ViewUniform, the uniform data structure containing
5372
    // the Camera data for the current view. If not available, we cannot render
5373
    // anything.
5374
    let Some(view_binding) = view_uniforms.uniforms.binding() else {
×
5375
        return;
×
5376
    };
5377

5378
    // Create the bind group for the camera/view parameters
5379
    // FIXME - Not here!
5380
    effects_meta.view_bind_group = Some(render_device.create_bind_group(
5381
        "hanabi:bind_group_camera_view",
5382
        &render_pipeline.view_layout,
5383
        &[
5384
            BindGroupEntry {
5385
                binding: 0,
5386
                resource: view_binding,
5387
            },
5388
            BindGroupEntry {
5389
                binding: 1,
5390
                resource: effects_meta.sim_params_uniforms.binding().unwrap(),
5391
            },
5392
        ],
5393
    ));
5394

5395
    // Re-/allocate any GPU buffer if needed
5396
    //effect_cache.prepare_buffers(&render_device, &render_queue, &mut
5397
    // effect_bind_groups);
5398
    event_cache.prepare_buffers(&render_device, &render_queue, &mut effect_bind_groups);
5399
    sort_bind_groups.prepare_buffers(&render_device);
5400
    if effects_meta
5401
        .update_dispatch_indirect_buffer
5402
        .prepare_buffers(&render_device)
5403
    {
5404
        // All those bind groups use the buffer so need to be re-created
NEW
5405
        trace!("*** Dispatch indirect buffer for update pass re-allocated; clearing all bind groups using it.");
×
NEW
5406
        effect_bind_groups.particle_buffers.clear();
×
5407
    }
5408
}
5409

5410
/// Read the queued init fill dispatch operations, batch them together by
5411
/// contiguous source and destination entries in the buffers, and enqueue
5412
/// corresponding GPU buffer fill dispatch operations for all batches.
5413
///
5414
/// This system runs after the GPU buffers have been (re-)allocated in
5415
/// [`prepare_gpu_resources()`], so that it can read the new buffer IDs and
5416
/// reference them from the generic [`GpuBufferOperationQueue`].
5417
pub(crate) fn queue_init_fill_dispatch_ops(
×
5418
    event_cache: Res<EventCache>,
5419
    render_device: Res<RenderDevice>,
5420
    render_queue: Res<RenderQueue>,
5421
    mut init_fill_dispatch_queue: ResMut<InitFillDispatchQueue>,
5422
    mut gpu_buffer_operations: ResMut<GpuBufferOperations>,
5423
) {
5424
    // Submit all queued init fill dispatch operations with the proper buffers
5425
    if !init_fill_dispatch_queue.is_empty() {
×
5426
        let src_buffer = event_cache.child_infos().buffer();
×
5427
        let dst_buffer = event_cache.init_indirect_dispatch_buffer();
×
5428
        if let (Some(src_buffer), Some(dst_buffer)) = (src_buffer, dst_buffer) {
×
5429
            init_fill_dispatch_queue.submit(src_buffer, dst_buffer, &mut gpu_buffer_operations);
5430
        } else {
5431
            if src_buffer.is_none() {
×
5432
                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());
×
5433
            }
5434
            if dst_buffer.is_none() {
×
5435
                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());
×
5436
            }
5437
        }
5438
    }
5439

5440
    // Once all GPU operations for this frame are enqueued, upload them to GPU
5441
    gpu_buffer_operations.end_frame(&render_device, &render_queue);
×
5442
}
5443

5444
pub(crate) fn prepare_bind_groups(
×
5445
    mut effects_meta: ResMut<EffectsMeta>,
5446
    mut effect_cache: ResMut<EffectCache>,
5447
    mut event_cache: ResMut<EventCache>,
5448
    mut effect_bind_groups: ResMut<EffectBindGroups>,
5449
    mut property_bind_groups: ResMut<PropertyBindGroups>,
5450
    mut sort_bind_groups: ResMut<SortBindGroups>,
5451
    property_cache: Res<PropertyCache>,
5452
    sorted_effect_batched: Res<SortedEffectBatches>,
5453
    render_device: Res<RenderDevice>,
5454
    dispatch_indirect_pipeline: Res<DispatchIndirectPipeline>,
5455
    utils_pipeline: Res<UtilsPipeline>,
5456
    update_pipeline: Res<ParticlesUpdatePipeline>,
5457
    render_pipeline: ResMut<ParticlesRenderPipeline>,
5458
    gpu_images: Res<RenderAssets<GpuImage>>,
5459
    mut gpu_buffer_operation_queue: ResMut<GpuBufferOperations>,
5460
) {
5461
    // We can't simulate nor render anything without at least the spawner buffer
5462
    if effects_meta.spawner_buffer.is_empty() {
×
5463
        return;
×
5464
    }
5465
    let Some(spawner_buffer) = effects_meta.spawner_buffer.buffer().cloned() else {
×
5466
        return;
×
5467
    };
5468

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

5474
    {
5475
        #[cfg(feature = "trace")]
5476
        let _span = bevy::utils::tracing::info_span!("shared_bind_groups").entered();
5477

5478
        // Make a copy of the buffer IDs before borrowing effects_meta mutably in the
5479
        // loop below. Also allows earlying out before doing any work in case some
5480
        // buffer is missing.
5481
        let Some(spawner_buffer) = effects_meta.spawner_buffer.buffer().cloned() else {
×
5482
            return;
×
5483
        };
5484

5485
        // Create the sim_params@0 bind group for the global simulation parameters,
5486
        // which is shared by the init and update passes.
5487
        if effects_meta.indirect_sim_params_bind_group.is_none() {
×
5488
            effects_meta.indirect_sim_params_bind_group = Some(render_device.create_bind_group(
×
5489
                "hanabi:bind_group:vfx_indirect:sim_params@0",
×
5490
                &update_pipeline.sim_params_layout, // FIXME - Shared with init
×
5491
                &[BindGroupEntry {
×
5492
                    binding: 0,
×
5493
                    resource: effects_meta.sim_params_uniforms.binding().unwrap(),
×
5494
                }],
5495
            ));
5496
        }
5497

5498
        // Create the @1 bind group for the indirect dispatch preparation pass of all
5499
        // effects at once
5500
        effects_meta.indirect_metadata_bind_group = match (
5501
            effects_meta.effect_metadata_buffer.buffer(),
5502
            effects_meta.update_dispatch_indirect_buffer.buffer(),
5503
        ) {
5504
            (Some(effect_metadata_buffer), Some(dispatch_indirect_buffer)) => {
×
5505
                // Base bind group for indirect pass
5506
                Some(render_device.create_bind_group(
×
5507
                    "hanabi:bind_group:vfx_indirect:metadata@1",
×
5508
                    &dispatch_indirect_pipeline.effect_metadata_bind_group_layout,
×
5509
                    &[
×
5510
                        // @group(1) @binding(0) var<storage, read_write> effect_metadata_buffer : array<u32>;
5511
                        BindGroupEntry {
×
5512
                            binding: 0,
×
5513
                            resource: BindingResource::Buffer(BufferBinding {
×
5514
                                buffer: effect_metadata_buffer,
×
5515
                                offset: 0,
×
5516
                                size: None, //NonZeroU64::new(256), // Some(GpuEffectMetadata::min_size()),
×
5517
                            }),
5518
                        },
5519
                        // @group(1) @binding(1) var<storage, read_write> dispatch_indirect_buffer : array<u32>;
5520
                        BindGroupEntry {
×
5521
                            binding: 1,
×
5522
                            resource: BindingResource::Buffer(BufferBinding {
×
5523
                                buffer: dispatch_indirect_buffer,
×
5524
                                offset: 0,
×
5525
                                size: None, //NonZeroU64::new(256), // Some(GpuDispatchIndirect::min_size()),
×
5526
                            }),
5527
                        },
5528
                    ],
5529
                ))
5530
            }
5531

5532
            // Some buffer is not yet available, can't create the bind group
5533
            _ => None,
×
5534
        };
5535

5536
        // Create the @2 bind group for the indirect dispatch preparation pass of all
5537
        // effects at once
5538
        if effects_meta.indirect_spawner_bind_group.is_none() {
×
5539
            let bind_group = render_device.create_bind_group(
×
5540
                "hanabi:bind_group:vfx_indirect:spawner@2",
5541
                &dispatch_indirect_pipeline.spawner_bind_group_layout,
×
5542
                &[
×
5543
                    // @group(2) @binding(0) var<storage, read> spawner_buffer : array<Spawner>;
5544
                    BindGroupEntry {
×
5545
                        binding: 0,
×
5546
                        resource: BindingResource::Buffer(BufferBinding {
×
5547
                            buffer: &spawner_buffer,
×
5548
                            offset: 0,
×
5549
                            size: None,
×
5550
                        }),
5551
                    },
5552
                ],
5553
            );
5554

5555
            effects_meta.indirect_spawner_bind_group = Some(bind_group);
×
5556
        }
5557
    }
5558

5559
    // Create the per-buffer bind groups
5560
    trace!("Create per-buffer bind groups...");
×
5561
    for (buffer_index, effect_buffer) in effect_cache.buffers().iter().enumerate() {
×
5562
        #[cfg(feature = "trace")]
5563
        let _span_buffer = bevy::utils::tracing::info_span!("create_buffer_bind_groups").entered();
5564

5565
        let Some(effect_buffer) = effect_buffer else {
×
5566
            trace!(
×
5567
                "Effect buffer index #{} has no allocated EffectBuffer, skipped.",
×
5568
                buffer_index
5569
            );
5570
            continue;
×
5571
        };
5572

5573
        // Ensure all effects in this batch have a bind group for the entire buffer of
5574
        // the group, since the update phase runs on an entire group/buffer at once,
5575
        // with all the effect instances in it batched together.
5576
        trace!("effect particle buffer_index=#{}", buffer_index);
×
5577
        effect_bind_groups
5578
            .particle_buffers
5579
            .entry(buffer_index as u32)
5580
            .or_insert_with(|| {
×
5581
                // Bind group particle@1 for render pass
5582
                trace!("Creating particle@1 bind group for buffer #{buffer_index} in render pass");
×
5583
                let spawner_min_binding_size = GpuSpawnerParams::aligned_size(
×
5584
                    render_device.limits().min_storage_buffer_offset_alignment,
×
5585
                );
5586
                let entries = [
×
5587
                    // @group(1) @binding(0) var<storage, read> particle_buffer : ParticleBuffer;
5588
                    BindGroupEntry {
×
5589
                        binding: 0,
×
5590
                        resource: effect_buffer.max_binding(),
×
5591
                    },
5592
                    // @group(1) @binding(1) var<storage, read> indirect_buffer : IndirectBuffer;
5593
                    BindGroupEntry {
×
5594
                        binding: 1,
×
5595
                        resource: effect_buffer.indirect_index_max_binding(),
×
5596
                    },
5597
                    // @group(1) @binding(2) var<storage, read> spawner : Spawner;
5598
                    BindGroupEntry {
×
5599
                        binding: 2,
×
5600
                        resource: BindingResource::Buffer(BufferBinding {
×
5601
                            buffer: &spawner_buffer,
×
5602
                            offset: 0,
×
5603
                            size: Some(spawner_min_binding_size),
×
5604
                        }),
5605
                    },
5606
                ];
5607
                let render = render_device.create_bind_group(
×
5608
                    &format!("hanabi:bind_group:render:particles@1:vfx{buffer_index}")[..],
×
5609
                    effect_buffer.render_particles_buffer_layout(),
×
5610
                    &entries[..],
×
5611
                );
5612

5613
                BufferBindGroups { render }
×
5614
            });
5615
    }
5616

5617
    // Create bind groups for queued GPU buffer operations
5618
    gpu_buffer_operation_queue.create_bind_groups(&render_device, &utils_pipeline);
5619

5620
    // Create the per-effect bind groups
5621
    let spawner_buffer_binding_size =
5622
        NonZeroU64::new(effects_meta.spawner_buffer.aligned_size() as u64).unwrap();
5623
    for effect_batch in sorted_effect_batched.iter() {
×
5624
        #[cfg(feature = "trace")]
5625
        let _span_buffer = bevy::utils::tracing::info_span!("create_batch_bind_groups").entered();
×
5626

5627
        // Create the property bind group @2 if needed
5628
        if let Some(property_key) = &effect_batch.property_key {
×
5629
            if let Err(err) = property_bind_groups.ensure_exists(
×
5630
                property_key,
5631
                &property_cache,
5632
                &spawner_buffer,
5633
                spawner_buffer_binding_size,
5634
                &render_device,
5635
            ) {
5636
                error!("Failed to create property bind group for effect batch: {err:?}");
×
5637
                continue;
5638
            }
5639
        } else if let Err(err) = property_bind_groups.ensure_exists_no_property(
×
5640
            &property_cache,
×
5641
            &spawner_buffer,
×
5642
            spawner_buffer_binding_size,
×
5643
            &render_device,
×
5644
        ) {
5645
            error!("Failed to create property bind group for effect batch: {err:?}");
×
5646
            continue;
5647
        }
5648

5649
        // Bind group particle@1 for the simulate compute shaders (init and udpate) to
5650
        // simulate particles.
5651
        if effect_cache
×
5652
            .create_particle_sim_bind_group(
5653
                effect_batch.buffer_index,
×
5654
                &render_device,
×
5655
                effect_batch.particle_layout.min_binding_size32(),
×
5656
                effect_batch.parent_min_binding_size,
×
5657
                effect_batch.parent_binding_source.as_ref(),
×
5658
            )
5659
            .is_err()
5660
        {
5661
            error!("No particle buffer allocated for effect batch.");
×
5662
            continue;
×
5663
        }
5664

5665
        // Bind group @3 of init pass
5666
        // FIXME - this is instance-dependent, not buffer-dependent
5667
        {
5668
            let consume_gpu_spawn_events = effect_batch
×
5669
                .layout_flags
×
5670
                .contains(LayoutFlags::CONSUME_GPU_SPAWN_EVENTS);
×
5671
            let consume_event_buffers = if let BatchSpawnInfo::GpuSpawner { .. } =
×
5672
                effect_batch.spawn_info
5673
            {
5674
                assert!(consume_gpu_spawn_events);
×
5675
                let cached_effect_events = effect_batch.cached_effect_events.as_ref().unwrap();
×
5676
                Some(ConsumeEventBuffers {
×
5677
                    child_infos_buffer: event_cache.child_infos_buffer().unwrap(),
×
5678
                    events: BufferSlice {
×
5679
                        buffer: event_cache
×
5680
                            .get_buffer(cached_effect_events.buffer_index)
×
5681
                            .unwrap(),
×
5682
                        // Note: event range is in u32 count, not bytes
5683
                        offset: cached_effect_events.range.start * 4,
×
5684
                        size: NonZeroU32::new(cached_effect_events.range.len() as u32 * 4).unwrap(),
×
5685
                    },
5686
                })
5687
            } else {
5688
                assert!(!consume_gpu_spawn_events);
×
5689
                None
×
5690
            };
5691
            let Some(init_metadata_layout) =
×
5692
                effect_cache.metadata_init_bind_group_layout(consume_gpu_spawn_events)
5693
            else {
5694
                continue;
×
5695
            };
5696
            if effect_bind_groups
5697
                .get_or_create_init_metadata(
5698
                    effect_batch,
5699
                    &effects_meta.gpu_limits,
5700
                    &render_device,
5701
                    init_metadata_layout,
5702
                    effects_meta.effect_metadata_buffer.buffer().unwrap(),
5703
                    consume_event_buffers,
5704
                )
5705
                .is_err()
5706
            {
5707
                continue;
×
5708
            }
5709
        }
5710

5711
        // Bind group @3 of update pass
5712
        // FIXME - this is instance-dependent, not buffer-dependent#
5713
        {
5714
            let num_event_buffers = effect_batch.child_event_buffers.len() as u32;
×
5715

5716
            let Some(update_metadata_layout) =
×
5717
                effect_cache.metadata_update_bind_group_layout(num_event_buffers)
5718
            else {
5719
                continue;
×
5720
            };
5721
            if effect_bind_groups
5722
                .get_or_create_update_metadata(
5723
                    effect_batch,
5724
                    &effects_meta.gpu_limits,
5725
                    &render_device,
5726
                    update_metadata_layout,
5727
                    effects_meta.effect_metadata_buffer.buffer().unwrap(),
5728
                    event_cache.child_infos_buffer(),
5729
                    &effect_batch.child_event_buffers[..],
5730
                )
5731
                .is_err()
5732
            {
5733
                continue;
×
5734
            }
5735
        }
5736

5737
        if effect_batch.layout_flags.contains(LayoutFlags::RIBBONS) {
×
5738
            let effect_buffer = effect_cache.get_buffer(effect_batch.buffer_index).unwrap();
×
5739

5740
            // Bind group @0 of sort-fill pass
5741
            let particle_buffer = effect_buffer.particle_buffer();
×
5742
            let indirect_index_buffer = effect_buffer.indirect_index_buffer();
×
5743
            let effect_metadata_buffer = effects_meta.effect_metadata_buffer.buffer().unwrap();
×
5744
            if let Err(err) = sort_bind_groups.ensure_sort_fill_bind_group(
×
5745
                &effect_batch.particle_layout,
×
5746
                particle_buffer,
×
5747
                indirect_index_buffer,
×
5748
                effect_metadata_buffer,
×
5749
            ) {
5750
                error!(
5751
                    "Failed to create sort-fill bind group @0 for ribbon effect: {:?}",
×
5752
                    err
5753
                );
5754
                continue;
5755
            }
5756

5757
            // Bind group @0 of sort-copy pass
5758
            let indirect_index_buffer = effect_buffer.indirect_index_buffer();
×
5759
            if let Err(err) = sort_bind_groups
×
5760
                .ensure_sort_copy_bind_group(indirect_index_buffer, effect_metadata_buffer)
×
5761
            {
5762
                error!(
5763
                    "Failed to create sort-copy bind group @0 for ribbon effect: {:?}",
×
5764
                    err
5765
                );
5766
                continue;
5767
            }
5768
        }
5769

5770
        // Ensure the particle texture(s) are available as GPU resources and that a bind
5771
        // group for them exists
5772
        // FIXME fix this insert+get below
5773
        if !effect_batch.texture_layout.layout.is_empty() {
×
5774
            // This should always be available, as this is cached into the render pipeline
5775
            // just before we start specializing it.
5776
            let Some(material_bind_group_layout) =
×
5777
                render_pipeline.get_material(&effect_batch.texture_layout)
×
5778
            else {
5779
                error!(
×
5780
                    "Failed to find material bind group layout for buffer #{}",
×
5781
                    effect_batch.buffer_index
5782
                );
5783
                continue;
×
5784
            };
5785

5786
            // TODO = move
5787
            let material = Material {
5788
                layout: effect_batch.texture_layout.clone(),
5789
                textures: effect_batch.textures.iter().map(|h| h.id()).collect(),
×
5790
            };
5791
            assert_eq!(material.layout.layout.len(), material.textures.len());
5792

5793
            //let bind_group_entries = material.make_entries(&gpu_images).unwrap();
5794
            let Ok(bind_group_entries) = material.make_entries(&gpu_images) else {
×
5795
                trace!(
×
5796
                    "Temporarily ignoring material {:?} due to missing image(s)",
×
5797
                    material
5798
                );
5799
                continue;
×
5800
            };
5801

5802
            effect_bind_groups
5803
                .material_bind_groups
5804
                .entry(material.clone())
5805
                .or_insert_with(|| {
×
5806
                    debug!("Creating material bind group for material {:?}", material);
×
5807
                    render_device.create_bind_group(
×
5808
                        &format!(
×
5809
                            "hanabi:material_bind_group_{}",
×
5810
                            material.layout.layout.len()
×
5811
                        )[..],
×
5812
                        material_bind_group_layout,
×
5813
                        &bind_group_entries[..],
×
5814
                    )
5815
                });
5816
        }
5817
    }
5818
}
5819

5820
type DrawEffectsSystemState = SystemState<(
5821
    SRes<EffectsMeta>,
5822
    SRes<EffectBindGroups>,
5823
    SRes<PipelineCache>,
5824
    SRes<RenderAssets<RenderMesh>>,
5825
    SRes<MeshAllocator>,
5826
    SQuery<Read<ViewUniformOffset>>,
5827
    SRes<SortedEffectBatches>,
5828
    SQuery<Read<EffectDrawBatch>>,
5829
)>;
5830

5831
/// Draw function for rendering all active effects for the current frame.
5832
///
5833
/// Effects are rendered in the [`Transparent2d`] phase of the main 2D pass,
5834
/// and the [`Transparent3d`] phase of the main 3D pass.
5835
pub(crate) struct DrawEffects {
5836
    params: DrawEffectsSystemState,
5837
}
5838

5839
impl DrawEffects {
5840
    pub fn new(world: &mut World) -> Self {
×
5841
        Self {
5842
            params: SystemState::new(world),
×
5843
        }
5844
    }
5845
}
5846

5847
/// Draw all particles of a single effect in view, in 2D or 3D.
5848
///
5849
/// FIXME: use pipeline ID to look up which group index it is.
5850
fn draw<'w>(
×
5851
    world: &'w World,
5852
    pass: &mut TrackedRenderPass<'w>,
5853
    view: Entity,
5854
    entity: (Entity, MainEntity),
5855
    pipeline_id: CachedRenderPipelineId,
5856
    params: &mut DrawEffectsSystemState,
5857
) {
5858
    let (
×
5859
        effects_meta,
×
5860
        effect_bind_groups,
×
5861
        pipeline_cache,
×
5862
        meshes,
×
5863
        mesh_allocator,
×
5864
        views,
×
5865
        sorted_effect_batches,
×
5866
        effect_draw_batches,
×
5867
    ) = params.get(world);
×
5868
    let view_uniform = views.get(view).unwrap();
×
5869
    let effects_meta = effects_meta.into_inner();
×
5870
    let effect_bind_groups = effect_bind_groups.into_inner();
×
5871
    let meshes = meshes.into_inner();
×
5872
    let mesh_allocator = mesh_allocator.into_inner();
×
5873
    let effect_draw_batch = effect_draw_batches.get(entity.0).unwrap();
×
5874
    let effect_batch = sorted_effect_batches
×
5875
        .get(effect_draw_batch.effect_batch_index)
×
5876
        .unwrap();
5877

5878
    let gpu_limits = &effects_meta.gpu_limits;
×
5879

5880
    let Some(pipeline) = pipeline_cache.into_inner().get_render_pipeline(pipeline_id) else {
×
5881
        return;
×
5882
    };
5883

5884
    trace!("render pass");
×
5885

5886
    pass.set_render_pipeline(pipeline);
×
5887

5888
    let Some(render_mesh): Option<&RenderMesh> = meshes.get(effect_batch.mesh) else {
×
5889
        return;
×
5890
    };
5891
    let Some(vertex_buffer_slice) = mesh_allocator.mesh_vertex_slice(&effect_batch.mesh) else {
×
5892
        return;
×
5893
    };
5894

5895
    // Vertex buffer containing the particle model to draw. Generally a quad.
5896
    // FIXME - need to upload "vertex_buffer_slice.range.start as i32" into
5897
    // "base_vertex" in the indirect struct...
5898
    assert_eq!(effect_batch.mesh_buffer_id, vertex_buffer_slice.buffer.id());
×
5899
    assert_eq!(effect_batch.mesh_slice, vertex_buffer_slice.range);
×
5900
    pass.set_vertex_buffer(0, vertex_buffer_slice.buffer.slice(..));
×
5901

5902
    // View properties (camera matrix, etc.)
5903
    pass.set_bind_group(
×
5904
        0,
5905
        effects_meta.view_bind_group.as_ref().unwrap(),
×
5906
        &[view_uniform.offset],
×
5907
    );
5908

5909
    // Particles buffer
5910
    let spawner_base = effect_batch.spawner_base;
×
5911
    let spawner_buffer_aligned = effects_meta.spawner_buffer.aligned_size();
×
5912
    assert!(spawner_buffer_aligned >= GpuSpawnerParams::min_size().get() as usize);
×
5913
    let spawner_offset = spawner_base * spawner_buffer_aligned as u32;
×
5914
    pass.set_bind_group(
×
5915
        1,
5916
        effect_bind_groups
×
5917
            .particle_render(effect_batch.buffer_index)
×
5918
            .unwrap(),
×
5919
        &[spawner_offset],
×
5920
    );
5921

5922
    // Particle texture
5923
    // TODO = move
5924
    let material = Material {
5925
        layout: effect_batch.texture_layout.clone(),
×
5926
        textures: effect_batch.textures.iter().map(|h| h.id()).collect(),
×
5927
    };
5928
    if !effect_batch.texture_layout.layout.is_empty() {
×
5929
        if let Some(bind_group) = effect_bind_groups.material_bind_groups.get(&material) {
×
5930
            pass.set_bind_group(2, bind_group, &[]);
×
5931
        } else {
5932
            // Texture(s) not ready; skip this drawing for now
5933
            trace!(
×
5934
                "Particle material bind group not available for batch buf={}. Skipping draw call.",
×
5935
                effect_batch.buffer_index,
×
5936
            );
5937
            return;
×
5938
        }
5939
    }
5940

5941
    let effect_metadata_index = effect_batch
×
5942
        .dispatch_buffer_indices
×
5943
        .effect_metadata_buffer_table_id
×
5944
        .0;
×
5945
    let effect_metadata_offset =
×
5946
        effect_metadata_index as u64 * gpu_limits.effect_metadata_aligned_size.get() as u64;
×
5947
    trace!(
×
5948
        "Draw up to {} particles with {} vertices per particle for batch from buffer #{} \
×
5949
            (effect_metadata_index={}, offset={}B).",
×
5950
        effect_batch.slice.len(),
×
5951
        render_mesh.vertex_count,
×
5952
        effect_batch.buffer_index,
×
5953
        effect_metadata_index,
×
5954
        effect_metadata_offset,
×
5955
    );
5956

5957
    // Note: the indirect draw args are the first few fields of GpuEffectMetadata
5958
    let Some(indirect_buffer) = effects_meta.effect_metadata_buffer.buffer() else {
×
5959
        trace!(
×
5960
            "The metadata buffer containing the indirect draw args is not ready for batch buf=#{}. Skipping draw call.",
×
5961
            effect_batch.buffer_index,
×
5962
        );
5963
        return;
×
5964
    };
5965

5966
    match render_mesh.buffer_info {
×
5967
        RenderMeshBufferInfo::Indexed { index_format, .. } => {
×
5968
            let Some(index_buffer_slice) = mesh_allocator.mesh_index_slice(&effect_batch.mesh)
×
5969
            else {
×
5970
                return;
×
5971
            };
5972

5973
            pass.set_index_buffer(index_buffer_slice.buffer.slice(..), 0, index_format);
×
5974
            pass.draw_indexed_indirect(indirect_buffer, effect_metadata_offset);
×
5975
        }
5976
        RenderMeshBufferInfo::NonIndexed => {
×
5977
            pass.draw_indirect(indirect_buffer, effect_metadata_offset);
×
5978
        }
5979
    }
5980
}
5981

5982
#[cfg(feature = "2d")]
5983
impl Draw<Transparent2d> for DrawEffects {
5984
    fn draw<'w>(
×
5985
        &mut self,
5986
        world: &'w World,
5987
        pass: &mut TrackedRenderPass<'w>,
5988
        view: Entity,
5989
        item: &Transparent2d,
5990
    ) -> Result<(), DrawError> {
5991
        trace!("Draw<Transparent2d>: view={:?}", view);
×
5992
        draw(
5993
            world,
×
5994
            pass,
×
5995
            view,
×
5996
            item.entity,
×
5997
            item.pipeline,
×
5998
            &mut self.params,
×
5999
        );
6000
        Ok(())
×
6001
    }
6002
}
6003

6004
#[cfg(feature = "3d")]
6005
impl Draw<Transparent3d> for DrawEffects {
6006
    fn draw<'w>(
×
6007
        &mut self,
6008
        world: &'w World,
6009
        pass: &mut TrackedRenderPass<'w>,
6010
        view: Entity,
6011
        item: &Transparent3d,
6012
    ) -> Result<(), DrawError> {
6013
        trace!("Draw<Transparent3d>: view={:?}", view);
×
6014
        draw(
6015
            world,
×
6016
            pass,
×
6017
            view,
×
6018
            item.entity,
×
6019
            item.pipeline,
×
6020
            &mut self.params,
×
6021
        );
6022
        Ok(())
×
6023
    }
6024
}
6025

6026
#[cfg(feature = "3d")]
6027
impl Draw<AlphaMask3d> for DrawEffects {
6028
    fn draw<'w>(
×
6029
        &mut self,
6030
        world: &'w World,
6031
        pass: &mut TrackedRenderPass<'w>,
6032
        view: Entity,
6033
        item: &AlphaMask3d,
6034
    ) -> Result<(), DrawError> {
6035
        trace!("Draw<AlphaMask3d>: view={:?}", view);
×
6036
        draw(
6037
            world,
×
6038
            pass,
×
6039
            view,
×
6040
            item.representative_entity,
×
6041
            item.key.pipeline,
×
6042
            &mut self.params,
×
6043
        );
6044
        Ok(())
×
6045
    }
6046
}
6047

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

6070
/// Render node to run the simulation sub-graph once per frame.
6071
///
6072
/// This node doesn't simulate anything by itself, but instead schedules the
6073
/// simulation sub-graph, where other nodes like [`VfxSimulateNode`] do the
6074
/// actual simulation.
6075
///
6076
/// The simulation sub-graph is scheduled to run before the [`CameraDriverNode`]
6077
/// renders all the views, such that rendered views have access to the
6078
/// just-simulated particles to render them.
6079
///
6080
/// [`CameraDriverNode`]: bevy::render::camera::CameraDriverNode
6081
pub(crate) struct VfxSimulateDriverNode;
6082

6083
impl Node for VfxSimulateDriverNode {
6084
    fn run(
×
6085
        &self,
6086
        graph: &mut RenderGraphContext,
6087
        _render_context: &mut RenderContext,
6088
        _world: &World,
6089
    ) -> Result<(), NodeRunError> {
6090
        graph.run_sub_graph(
×
6091
            crate::plugin::simulate_graph::HanabiSimulateGraph,
×
6092
            vec![],
×
6093
            None,
×
6094
        )?;
6095
        Ok(())
×
6096
    }
6097
}
6098

6099
#[derive(Debug, Clone, PartialEq, Eq)]
6100
enum HanabiPipelineId {
6101
    Invalid,
6102
    Cached(CachedComputePipelineId),
6103
}
6104

6105
pub(crate) enum ComputePipelineError {
6106
    Queued,
6107
    Creating,
6108
    Error,
6109
}
6110

6111
impl From<&CachedPipelineState> for ComputePipelineError {
6112
    fn from(value: &CachedPipelineState) -> Self {
×
6113
        match value {
×
6114
            CachedPipelineState::Queued => Self::Queued,
×
6115
            CachedPipelineState::Creating(_) => Self::Creating,
×
6116
            CachedPipelineState::Err(_) => Self::Error,
×
6117
            _ => panic!("Trying to convert Ok state to error."),
×
6118
        }
6119
    }
6120
}
6121

6122
pub(crate) struct HanabiComputePass<'a> {
6123
    /// Pipeline cache to fetch cached compute pipelines by ID.
6124
    pipeline_cache: &'a PipelineCache,
6125
    /// WGPU compute pass.
6126
    compute_pass: ComputePass<'a>,
6127
    /// Current pipeline (cached).
6128
    pipeline_id: HanabiPipelineId,
6129
}
6130

6131
impl<'a> Deref for HanabiComputePass<'a> {
6132
    type Target = ComputePass<'a>;
6133

6134
    fn deref(&self) -> &Self::Target {
×
6135
        &self.compute_pass
×
6136
    }
6137
}
6138

6139
impl DerefMut for HanabiComputePass<'_> {
6140
    fn deref_mut(&mut self) -> &mut Self::Target {
×
6141
        &mut self.compute_pass
×
6142
    }
6143
}
6144

6145
impl<'a> HanabiComputePass<'a> {
6146
    pub fn new(pipeline_cache: &'a PipelineCache, compute_pass: ComputePass<'a>) -> Self {
×
6147
        Self {
6148
            pipeline_cache,
6149
            compute_pass,
6150
            pipeline_id: HanabiPipelineId::Invalid,
6151
        }
6152
    }
6153

6154
    pub fn set_cached_compute_pipeline(
×
6155
        &mut self,
6156
        pipeline_id: CachedComputePipelineId,
6157
    ) -> Result<(), ComputePipelineError> {
6158
        trace!("set_cached_compute_pipeline() id={pipeline_id:?}");
×
6159
        if HanabiPipelineId::Cached(pipeline_id) == self.pipeline_id {
×
6160
            trace!("-> already set; skipped");
×
6161
            return Ok(());
×
6162
        }
6163
        let Some(pipeline) = self.pipeline_cache.get_compute_pipeline(pipeline_id) else {
×
6164
            let state = self.pipeline_cache.get_compute_pipeline_state(pipeline_id);
×
6165
            if let CachedPipelineState::Err(err) = state {
×
6166
                error!(
×
6167
                    "Failed to find compute pipeline #{}: {:?}",
×
6168
                    pipeline_id.id(),
×
6169
                    err
×
6170
                );
6171
            } else {
6172
                debug!("Compute pipeline not ready #{}", pipeline_id.id());
×
6173
            }
6174
            return Err(state.into());
×
6175
        };
6176
        self.compute_pass.set_pipeline(pipeline);
×
6177
        self.pipeline_id = HanabiPipelineId::Cached(pipeline_id);
×
6178
        Ok(())
×
6179
    }
6180
}
6181

6182
/// Render node to run the simulation of all effects once per frame.
6183
///
6184
/// Runs inside the simulation sub-graph, looping over all extracted effect
6185
/// batches to simulate them.
6186
pub(crate) struct VfxSimulateNode {}
6187

6188
impl VfxSimulateNode {
6189
    /// Create a new node for simulating the effects of the given world.
6190
    pub fn new(_world: &mut World) -> Self {
×
6191
        Self {}
6192
    }
6193

6194
    /// Begin a new compute pass and return a wrapper with extra
6195
    /// functionalities.
6196
    pub fn begin_compute_pass<'encoder>(
×
6197
        &self,
6198
        label: &str,
6199
        pipeline_cache: &'encoder PipelineCache,
6200
        render_context: &'encoder mut RenderContext,
6201
    ) -> HanabiComputePass<'encoder> {
6202
        let compute_pass =
×
6203
            render_context
×
6204
                .command_encoder()
6205
                .begin_compute_pass(&ComputePassDescriptor {
×
6206
                    label: Some(label),
×
6207
                    timestamp_writes: None,
×
6208
                });
6209
        HanabiComputePass::new(pipeline_cache, compute_pass)
×
6210
    }
6211
}
6212

6213
impl Node for VfxSimulateNode {
6214
    fn input(&self) -> Vec<SlotInfo> {
×
6215
        vec![]
×
6216
    }
6217

6218
    fn update(&mut self, _world: &mut World) {}
×
6219

6220
    fn run(
×
6221
        &self,
6222
        _graph: &mut RenderGraphContext,
6223
        render_context: &mut RenderContext,
6224
        world: &World,
6225
    ) -> Result<(), NodeRunError> {
6226
        trace!("VfxSimulateNode::run()");
×
6227

6228
        let pipeline_cache = world.resource::<PipelineCache>();
×
6229
        let effects_meta = world.resource::<EffectsMeta>();
×
6230
        let effect_bind_groups = world.resource::<EffectBindGroups>();
×
6231
        let property_bind_groups = world.resource::<PropertyBindGroups>();
×
6232
        let sort_bind_groups = world.resource::<SortBindGroups>();
×
6233
        let utils_pipeline = world.resource::<UtilsPipeline>();
×
6234
        let effect_cache = world.resource::<EffectCache>();
×
6235
        let event_cache = world.resource::<EventCache>();
×
6236
        let gpu_buffer_operations = world.resource::<GpuBufferOperations>();
×
6237
        let sorted_effect_batches = world.resource::<SortedEffectBatches>();
×
6238
        let init_fill_dispatch_queue = world.resource::<InitFillDispatchQueue>();
×
6239

6240
        // Make sure to schedule any buffer copy before accessing their content later in
6241
        // the GPU commands below.
6242
        {
6243
            let command_encoder = render_context.command_encoder();
×
6244
            effects_meta
×
6245
                .update_dispatch_indirect_buffer
×
NEW
6246
                .write_buffers(command_encoder);
×
6247
            effects_meta
×
6248
                .effect_metadata_buffer
×
6249
                .write_buffer(command_encoder);
×
NEW
6250
            event_cache.write_buffers(command_encoder);
×
UNCOV
6251
            sort_bind_groups.write_buffers(command_encoder);
×
6252
        }
6253

6254
        // Compute init fill dispatch pass - Fill the indirect dispatch structs for any
6255
        // upcoming init pass of this frame, based on the GPU spawn events emitted by
6256
        // the update pass of their parent effect during the previous frame.
6257
        if let Some(queue_index) = init_fill_dispatch_queue.submitted_queue_index.as_ref() {
×
6258
            gpu_buffer_operations.dispatch(
6259
                *queue_index,
6260
                render_context,
6261
                utils_pipeline,
6262
                Some("hanabi:init_indirect_fill_dispatch"),
6263
            );
6264
        }
6265

6266
        // If there's no batch, there's nothing more to do. Avoid continuing because
6267
        // some GPU resources are missing, which is expected when there's no effect but
6268
        // is an error (and will log warnings/errors) otherwise.
6269
        if sorted_effect_batches.is_empty() {
×
6270
            return Ok(());
×
6271
        }
6272

6273
        // Compute init pass
6274
        {
6275
            trace!("init: loop over effect batches...");
×
6276

6277
            let mut compute_pass =
6278
                self.begin_compute_pass("hanabi:init", pipeline_cache, render_context);
6279

6280
            // Bind group simparams@0 is common to everything, only set once per init pass
6281
            compute_pass.set_bind_group(
6282
                0,
6283
                effects_meta
6284
                    .indirect_sim_params_bind_group
6285
                    .as_ref()
6286
                    .unwrap(),
6287
                &[],
6288
            );
6289

6290
            // Dispatch init compute jobs for all batches
6291
            for effect_batch in sorted_effect_batches.iter() {
×
6292
                // Do not dispatch any init work if there's nothing to spawn this frame for the
6293
                // batch. Note that this hopefully should have been skipped earlier.
6294
                {
6295
                    let use_indirect_dispatch = effect_batch
×
6296
                        .layout_flags
×
6297
                        .contains(LayoutFlags::CONSUME_GPU_SPAWN_EVENTS);
×
6298
                    match effect_batch.spawn_info {
×
6299
                        BatchSpawnInfo::CpuSpawner { total_spawn_count } => {
×
6300
                            assert!(!use_indirect_dispatch);
×
6301
                            if total_spawn_count == 0 {
×
6302
                                continue;
×
6303
                            }
6304
                        }
6305
                        BatchSpawnInfo::GpuSpawner { .. } => {
6306
                            assert!(use_indirect_dispatch);
×
6307
                        }
6308
                    }
6309
                }
6310

6311
                // Fetch bind group particle@1
6312
                let Some(particle_bind_group) =
×
6313
                    effect_cache.particle_sim_bind_group(effect_batch.buffer_index)
×
6314
                else {
6315
                    error!(
×
6316
                        "Failed to find init particle@1 bind group for buffer index {}",
×
6317
                        effect_batch.buffer_index
6318
                    );
6319
                    continue;
×
6320
                };
6321

6322
                // Fetch bind group metadata@3
6323
                let Some(metadata_bind_group) = effect_bind_groups
×
6324
                    .init_metadata_bind_groups
6325
                    .get(&effect_batch.buffer_index)
6326
                else {
6327
                    error!(
×
6328
                        "Failed to find init metadata@3 bind group for buffer index {}",
×
6329
                        effect_batch.buffer_index
6330
                    );
6331
                    continue;
×
6332
                };
6333

6334
                if compute_pass
6335
                    .set_cached_compute_pipeline(effect_batch.init_and_update_pipeline_ids.init)
6336
                    .is_err()
6337
                {
6338
                    continue;
×
6339
                }
6340

6341
                // Compute dynamic offsets
6342
                let spawner_base = effect_batch.spawner_base;
×
6343
                let spawner_aligned_size = effects_meta.spawner_buffer.aligned_size();
×
6344
                debug_assert!(spawner_aligned_size >= GpuSpawnerParams::min_size().get() as usize);
×
6345
                let spawner_offset = spawner_base * spawner_aligned_size as u32;
×
6346
                let property_offset = effect_batch.property_offset;
×
6347

6348
                // Setup init pass
6349
                compute_pass.set_bind_group(1, particle_bind_group, &[]);
×
6350
                let offsets = if let Some(property_offset) = property_offset {
×
6351
                    vec![spawner_offset, property_offset]
6352
                } else {
6353
                    vec![spawner_offset]
×
6354
                };
6355
                compute_pass.set_bind_group(
×
6356
                    2,
6357
                    property_bind_groups
×
6358
                        .get(effect_batch.property_key.as_ref())
×
6359
                        .unwrap(),
×
6360
                    &offsets[..],
×
6361
                );
6362
                compute_pass.set_bind_group(3, &metadata_bind_group.bind_group, &[]);
×
6363

6364
                // Dispatch init job
6365
                match effect_batch.spawn_info {
×
6366
                    // Indirect dispatch via GPU spawn events
6367
                    BatchSpawnInfo::GpuSpawner {
6368
                        init_indirect_dispatch_index,
×
6369
                        ..
×
6370
                    } => {
×
6371
                        assert!(effect_batch
×
6372
                            .layout_flags
×
6373
                            .contains(LayoutFlags::CONSUME_GPU_SPAWN_EVENTS));
×
6374

6375
                        // Note: the indirect offset of a dispatch workgroup only needs
6376
                        // 4-byte alignment
6377
                        assert_eq!(GpuDispatchIndirect::min_size().get(), 12);
×
6378
                        let indirect_offset = init_indirect_dispatch_index as u64 * 12;
×
6379

6380
                        trace!(
×
6381
                            "record commands for indirect init pipeline of effect {:?} \
×
6382
                                init_indirect_dispatch_index={} \
×
6383
                                indirect_offset={} \
×
6384
                                spawner_base={} \
×
6385
                                spawner_offset={} \
×
6386
                                property_key={:?}...",
×
6387
                            effect_batch.handle,
6388
                            init_indirect_dispatch_index,
6389
                            indirect_offset,
6390
                            spawner_base,
6391
                            spawner_offset,
6392
                            effect_batch.property_key,
6393
                        );
6394

6395
                        compute_pass.dispatch_workgroups_indirect(
×
6396
                            event_cache.init_indirect_dispatch_buffer().unwrap(),
×
6397
                            indirect_offset,
×
6398
                        );
6399
                    }
6400

6401
                    // Direct dispatch via CPU spawn count
6402
                    BatchSpawnInfo::CpuSpawner {
6403
                        total_spawn_count: spawn_count,
×
6404
                    } => {
×
6405
                        assert!(!effect_batch
×
6406
                            .layout_flags
×
6407
                            .contains(LayoutFlags::CONSUME_GPU_SPAWN_EVENTS));
×
6408

6409
                        const WORKGROUP_SIZE: u32 = 64;
6410
                        let workgroup_count = spawn_count.div_ceil(WORKGROUP_SIZE);
×
6411

6412
                        trace!(
×
6413
                            "record commands for init pipeline of effect {:?} \
×
6414
                                (spawn {} particles => {} workgroups) spawner_base={} \
×
6415
                                spawner_offset={} \
×
6416
                                property_key={:?}...",
×
6417
                            effect_batch.handle,
6418
                            spawn_count,
6419
                            workgroup_count,
6420
                            spawner_base,
6421
                            spawner_offset,
6422
                            effect_batch.property_key,
6423
                        );
6424

6425
                        compute_pass.dispatch_workgroups(workgroup_count, 1, 1);
6426
                    }
6427
                }
6428

6429
                trace!("init compute dispatched");
×
6430
            }
6431
        }
6432

6433
        // Compute indirect dispatch pass
6434
        if effects_meta.spawner_buffer.buffer().is_some()
×
6435
            && !effects_meta.spawner_buffer.is_empty()
×
6436
            && effects_meta.indirect_metadata_bind_group.is_some()
×
6437
            && effects_meta.indirect_sim_params_bind_group.is_some()
×
6438
        {
6439
            // Only start a compute pass if there's an effect; makes things clearer in
6440
            // debugger.
6441
            let mut compute_pass =
×
6442
                self.begin_compute_pass("hanabi:indirect_dispatch", pipeline_cache, render_context);
×
6443

6444
            // Dispatch indirect dispatch compute job
6445
            trace!("record commands for indirect dispatch pipeline...");
×
6446

6447
            let has_gpu_spawn_events = !event_cache.child_infos().is_empty();
×
6448
            if has_gpu_spawn_events {
×
6449
                if let Some(indirect_child_info_buffer_bind_group) =
×
6450
                    event_cache.indirect_child_info_buffer_bind_group()
×
6451
                {
6452
                    assert!(has_gpu_spawn_events);
6453
                    compute_pass.set_bind_group(3, indirect_child_info_buffer_bind_group, &[]);
×
6454
                } else {
6455
                    error!("Missing child_info_buffer@3 bind group for the vfx_indirect pass.");
×
6456
                    render_context
×
6457
                        .command_encoder()
6458
                        .insert_debug_marker("ERROR:MissingIndirectBindGroup3");
6459
                    // FIXME - Bevy doesn't allow returning custom errors here...
6460
                    return Ok(());
×
6461
                }
6462
            }
6463

6464
            if compute_pass
×
6465
                .set_cached_compute_pipeline(effects_meta.active_indirect_pipeline_id)
×
6466
                .is_err()
6467
            {
6468
                // FIXME - Bevy doesn't allow returning custom errors here...
6469
                return Ok(());
×
6470
            }
6471

6472
            //error!("FIXME - effect_metadata_buffer has gaps!!!! this won't work. len() is
6473
            // the size exluding gaps!");
6474
            const WORKGROUP_SIZE: u32 = 64;
6475
            //let total_effect_count = effects_meta.effect_metadata_buffer.len();
6476
            let total_effect_count = effects_meta.spawner_buffer.len() as u32;
6477
            let workgroup_count = total_effect_count.div_ceil(WORKGROUP_SIZE);
6478

6479
            // Setup vfx_indirect pass
6480
            compute_pass.set_bind_group(
6481
                0,
6482
                effects_meta
6483
                    .indirect_sim_params_bind_group
6484
                    .as_ref()
6485
                    .unwrap(),
6486
                &[],
6487
            );
6488
            compute_pass.set_bind_group(
6489
                1,
6490
                // FIXME - got some unwrap() panic here, investigate... possibly race
6491
                // condition!
6492
                effects_meta.indirect_metadata_bind_group.as_ref().unwrap(),
6493
                &[],
6494
            );
6495
            compute_pass.set_bind_group(
6496
                2,
6497
                effects_meta.indirect_spawner_bind_group.as_ref().unwrap(),
6498
                &[],
6499
            );
6500
            compute_pass.dispatch_workgroups(workgroup_count, 1, 1);
6501
            trace!(
6502
                "indirect dispatch compute dispatched: total_effect_count={} workgroup_count={}",
×
6503
                total_effect_count,
6504
                workgroup_count
6505
            );
6506
        }
6507

6508
        // Compute update pass
6509
        {
6510
            let Some(indirect_buffer) = effects_meta.update_dispatch_indirect_buffer.buffer()
×
6511
            else {
6512
                warn!("Missing indirect buffer for update pass, cannot dispatch anything.");
×
6513
                render_context
×
6514
                    .command_encoder()
6515
                    .insert_debug_marker("ERROR:MissingUpdateIndirectBuffer");
6516
                // FIXME - Bevy doesn't allow returning custom errors here...
6517
                return Ok(());
×
6518
            };
6519

6520
            let mut compute_pass =
6521
                self.begin_compute_pass("hanabi:update", pipeline_cache, render_context);
6522

6523
            // Bind group simparams@0 is common to everything, only set once per update pass
6524
            compute_pass.set_bind_group(
6525
                0,
6526
                effects_meta
6527
                    .indirect_sim_params_bind_group
6528
                    .as_ref()
6529
                    .unwrap(),
6530
                &[],
6531
            );
6532

6533
            // Dispatch update compute jobs
6534
            for effect_batch in sorted_effect_batches.iter() {
×
6535
                // Fetch bind group particle@1
6536
                let Some(particle_bind_group) =
×
6537
                    effect_cache.particle_sim_bind_group(effect_batch.buffer_index)
×
6538
                else {
6539
                    error!(
×
6540
                        "Failed to find update particle@1 bind group for buffer index {}",
×
6541
                        effect_batch.buffer_index
6542
                    );
6543
                    continue;
×
6544
                };
6545

6546
                // Fetch bind group metadata@3
6547
                let Some(metadata_bind_group) = effect_bind_groups
×
6548
                    .update_metadata_bind_groups
6549
                    .get(&effect_batch.buffer_index)
6550
                else {
6551
                    error!(
×
6552
                        "Failed to find update metadata@3 bind group for buffer index {}",
×
6553
                        effect_batch.buffer_index
6554
                    );
6555
                    continue;
×
6556
                };
6557

6558
                // Fetch compute pipeline
6559
                if compute_pass
6560
                    .set_cached_compute_pipeline(effect_batch.init_and_update_pipeline_ids.update)
6561
                    .is_err()
6562
                {
6563
                    continue;
×
6564
                }
6565

6566
                // Compute dynamic offsets
6567
                let spawner_index = effect_batch.spawner_base;
×
6568
                let spawner_aligned_size = effects_meta.spawner_buffer.aligned_size();
×
6569
                assert!(spawner_aligned_size >= GpuSpawnerParams::min_size().get() as usize);
×
6570
                let spawner_offset = spawner_index * spawner_aligned_size as u32;
×
6571
                let property_offset = effect_batch.property_offset;
×
6572

6573
                trace!(
×
6574
                    "record commands for update pipeline of effect {:?} spawner_base={}",
×
6575
                    effect_batch.handle,
6576
                    spawner_index,
6577
                );
6578

6579
                // Setup update pass
6580
                compute_pass.set_bind_group(1, particle_bind_group, &[]);
6581
                let offsets = if let Some(property_offset) = property_offset {
×
6582
                    vec![spawner_offset, property_offset]
6583
                } else {
6584
                    vec![spawner_offset]
×
6585
                };
6586
                compute_pass.set_bind_group(
6587
                    2,
6588
                    property_bind_groups
6589
                        .get(effect_batch.property_key.as_ref())
6590
                        .unwrap(),
6591
                    &offsets[..],
6592
                );
6593
                compute_pass.set_bind_group(3, &metadata_bind_group.bind_group, &[]);
6594

6595
                // Dispatch update job
6596
                let dispatch_indirect_offset = effect_batch
6597
                    .dispatch_buffer_indices
6598
                    .update_dispatch_indirect_buffer_row_index
6599
                    * 12;
6600
                trace!(
6601
                    "dispatch_workgroups_indirect: buffer={:?} offset={}B",
×
6602
                    indirect_buffer,
6603
                    dispatch_indirect_offset,
6604
                );
6605
                compute_pass
6606
                    .dispatch_workgroups_indirect(indirect_buffer, dispatch_indirect_offset as u64);
6607

6608
                trace!("update compute dispatched");
×
6609
            }
6610
        }
6611

6612
        // Compute sort fill dispatch pass - Fill the indirect dispatch structs for any
6613
        // batch of particles which needs sorting, based on the actual number of alive
6614
        // particles in the batch after their update in the compute update pass. Since
6615
        // particles may die during update, this may be different from the number of
6616
        // particles updated.
6617
        if let Some(queue_index) = sorted_effect_batches.dispatch_queue_index.as_ref() {
×
6618
            gpu_buffer_operations.dispatch(
6619
                *queue_index,
6620
                render_context,
6621
                utils_pipeline,
6622
                Some("hanabi:sort_fill_dispatch"),
6623
            );
6624
        }
6625

6626
        // Compute sort pass
6627
        {
6628
            let mut compute_pass =
6629
                self.begin_compute_pass("hanabi:sort", pipeline_cache, render_context);
6630

6631
            let effect_metadata_buffer = effects_meta.effect_metadata_buffer.buffer().unwrap();
6632
            let indirect_buffer = sort_bind_groups.indirect_buffer().unwrap();
6633

6634
            // Loop on batches and find those which need sorting
6635
            for effect_batch in sorted_effect_batches.iter() {
×
6636
                trace!("Processing effect batch for sorting...");
×
6637
                if !effect_batch.layout_flags.contains(LayoutFlags::RIBBONS) {
×
6638
                    continue;
×
6639
                }
6640
                assert!(effect_batch.particle_layout.contains(Attribute::RIBBON_ID));
×
6641
                assert!(effect_batch.particle_layout.contains(Attribute::AGE)); // or is that optional?
×
6642

6643
                let Some(effect_buffer) = effect_cache.get_buffer(effect_batch.buffer_index) else {
×
6644
                    warn!("Missing sort-fill effect buffer.");
×
6645
                    continue;
×
6646
                };
6647

6648
                let indirect_dispatch_index = *effect_batch
6649
                    .sort_fill_indirect_dispatch_index
6650
                    .as_ref()
6651
                    .unwrap();
6652
                let indirect_offset =
6653
                    sort_bind_groups.get_indirect_dispatch_byte_offset(indirect_dispatch_index);
6654

6655
                // Fill the sort buffer with the key-value pairs to sort
6656
                {
6657
                    compute_pass.push_debug_group("hanabi:sort_fill");
6658

6659
                    // Fetch compute pipeline
6660
                    let Some(pipeline_id) =
×
6661
                        sort_bind_groups.get_sort_fill_pipeline_id(&effect_batch.particle_layout)
6662
                    else {
6663
                        warn!("Missing sort-fill pipeline.");
×
6664
                        continue;
×
6665
                    };
6666
                    if compute_pass
6667
                        .set_cached_compute_pipeline(pipeline_id)
6668
                        .is_err()
6669
                    {
6670
                        compute_pass.pop_debug_group();
×
6671
                        // FIXME - Bevy doesn't allow returning custom errors here...
6672
                        return Ok(());
×
6673
                    }
6674

6675
                    // Bind group sort_fill@0
6676
                    let particle_buffer = effect_buffer.particle_buffer();
×
6677
                    let indirect_index_buffer = effect_buffer.indirect_index_buffer();
×
6678
                    let Some(bind_group) = sort_bind_groups.sort_fill_bind_group(
×
6679
                        particle_buffer.id(),
6680
                        indirect_index_buffer.id(),
6681
                        effect_metadata_buffer.id(),
6682
                    ) else {
6683
                        warn!("Missing sort-fill bind group.");
×
6684
                        continue;
×
6685
                    };
6686
                    let particle_offset = effect_buffer.particle_offset(effect_batch.slice.start);
6687
                    let indirect_index_offset =
6688
                        effect_buffer.indirect_index_offset(effect_batch.slice.start);
6689
                    let effect_metadata_offset = effects_meta.gpu_limits.effect_metadata_offset(
6690
                        effect_batch
6691
                            .dispatch_buffer_indices
6692
                            .effect_metadata_buffer_table_id
6693
                            .0,
6694
                    ) as u32;
6695
                    compute_pass.set_bind_group(
6696
                        0,
6697
                        bind_group,
6698
                        &[
6699
                            particle_offset,
6700
                            indirect_index_offset,
6701
                            effect_metadata_offset,
6702
                        ],
6703
                    );
6704

6705
                    compute_pass
6706
                        .dispatch_workgroups_indirect(indirect_buffer, indirect_offset as u64);
6707
                    trace!("Dispatched sort-fill with indirect offset +{indirect_offset}");
×
6708

6709
                    compute_pass.pop_debug_group();
6710
                }
6711

6712
                // Do the actual sort
6713
                {
6714
                    compute_pass.push_debug_group("hanabi:sort");
6715

6716
                    if compute_pass
6717
                        .set_cached_compute_pipeline(sort_bind_groups.sort_pipeline_id())
6718
                        .is_err()
6719
                    {
6720
                        compute_pass.pop_debug_group();
×
6721
                        // FIXME - Bevy doesn't allow returning custom errors here...
6722
                        return Ok(());
×
6723
                    }
6724

6725
                    compute_pass.set_bind_group(0, sort_bind_groups.sort_bind_group(), &[]);
×
6726
                    compute_pass
×
6727
                        .dispatch_workgroups_indirect(indirect_buffer, indirect_offset as u64);
×
6728
                    trace!("Dispatched sort with indirect offset +{indirect_offset}");
×
6729

6730
                    compute_pass.pop_debug_group();
6731
                }
6732

6733
                // Copy the sorted particle indices back into the indirect index buffer, where
6734
                // the render pass will read them.
6735
                {
6736
                    compute_pass.push_debug_group("hanabi:copy_sorted_indices");
6737

6738
                    // Fetch compute pipeline
6739
                    let pipeline_id = sort_bind_groups.get_sort_copy_pipeline_id();
6740
                    if compute_pass
6741
                        .set_cached_compute_pipeline(pipeline_id)
6742
                        .is_err()
6743
                    {
6744
                        compute_pass.pop_debug_group();
×
6745
                        // FIXME - Bevy doesn't allow returning custom errors here...
6746
                        return Ok(());
×
6747
                    }
6748

6749
                    // Bind group sort_copy@0
6750
                    let indirect_index_buffer = effect_buffer.indirect_index_buffer();
×
6751
                    let Some(bind_group) = sort_bind_groups.sort_copy_bind_group(
×
6752
                        indirect_index_buffer.id(),
6753
                        effect_metadata_buffer.id(),
6754
                    ) else {
6755
                        warn!("Missing sort-copy bind group.");
×
6756
                        continue;
×
6757
                    };
6758
                    let indirect_index_offset = effect_batch.slice.start;
6759
                    let effect_metadata_offset =
6760
                        effects_meta.effect_metadata_buffer.dynamic_offset(
6761
                            effect_batch
6762
                                .dispatch_buffer_indices
6763
                                .effect_metadata_buffer_table_id,
6764
                        );
6765
                    compute_pass.set_bind_group(
6766
                        0,
6767
                        bind_group,
6768
                        &[indirect_index_offset, effect_metadata_offset],
6769
                    );
6770

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

6775
                    compute_pass.pop_debug_group();
6776
                }
6777
            }
6778
        }
6779

6780
        Ok(())
×
6781
    }
6782
}
6783

6784
impl From<LayoutFlags> for ParticleRenderAlphaMaskPipelineKey {
6785
    fn from(layout_flags: LayoutFlags) -> Self {
×
6786
        if layout_flags.contains(LayoutFlags::USE_ALPHA_MASK) {
×
6787
            ParticleRenderAlphaMaskPipelineKey::AlphaMask
×
6788
        } else if layout_flags.contains(LayoutFlags::OPAQUE) {
×
6789
            ParticleRenderAlphaMaskPipelineKey::Opaque
×
6790
        } else {
6791
            ParticleRenderAlphaMaskPipelineKey::Blend
×
6792
        }
6793
    }
6794
}
6795

6796
#[cfg(test)]
6797
mod tests {
6798
    use super::*;
6799

6800
    #[test]
6801
    fn layout_flags() {
6802
        let flags = LayoutFlags::default();
6803
        assert_eq!(flags, LayoutFlags::NONE);
6804
    }
6805

6806
    #[cfg(feature = "gpu_tests")]
6807
    #[test]
6808
    fn gpu_limits() {
6809
        use crate::test_utils::MockRenderer;
6810

6811
        let renderer = MockRenderer::new();
6812
        let device = renderer.device();
6813
        let limits = GpuLimits::from_device(&device);
6814

6815
        // assert!(limits.storage_buffer_align().get() >= 1);
6816
        assert!(limits.effect_metadata_offset(256) >= 256 * GpuEffectMetadata::min_size().get());
6817
    }
6818

6819
    #[cfg(feature = "gpu_tests")]
6820
    #[test]
6821
    fn gpu_ops_ifda() {
6822
        use crate::test_utils::MockRenderer;
6823

6824
        let renderer = MockRenderer::new();
6825
        let device = renderer.device();
6826
        let render_queue = renderer.queue();
6827

6828
        let mut world = World::new();
6829
        world.insert_resource(device.clone());
6830
        let mut buffer_ops = GpuBufferOperations::from_world(&mut world);
6831

6832
        let src_buffer = device.create_buffer(&BufferDescriptor {
6833
            label: None,
6834
            size: 256,
6835
            usage: BufferUsages::STORAGE,
6836
            mapped_at_creation: false,
6837
        });
6838
        let dst_buffer = device.create_buffer(&BufferDescriptor {
6839
            label: None,
6840
            size: 256,
6841
            usage: BufferUsages::STORAGE,
6842
            mapped_at_creation: false,
6843
        });
6844

6845
        // Two consecutive ops can be merged. This includes having contiguous slices
6846
        // both in source and destination.
6847
        buffer_ops.begin_frame();
6848
        {
6849
            let mut q = InitFillDispatchQueue::default();
6850
            q.enqueue(0, 0);
6851
            assert_eq!(q.queue.len(), 1);
6852
            q.enqueue(1, 1);
6853
            // Ops are not batched yet
6854
            assert_eq!(q.queue.len(), 2);
6855
            // On submit, the ops get batched together
6856
            q.submit(&src_buffer, &dst_buffer, &mut buffer_ops);
6857
            assert_eq!(buffer_ops.args_buffer.len(), 1);
6858
        }
6859
        buffer_ops.end_frame(&device, &render_queue);
6860

6861
        // Even if out of order, the init fill dispatch ops are batchable. Here the
6862
        // offsets are enqueued inverted.
6863
        buffer_ops.begin_frame();
6864
        {
6865
            let mut q = InitFillDispatchQueue::default();
6866
            q.enqueue(1, 1);
6867
            assert_eq!(q.queue.len(), 1);
6868
            q.enqueue(0, 0);
6869
            // Ops are not batched yet
6870
            assert_eq!(q.queue.len(), 2);
6871
            // On submit, the ops get batched together
6872
            q.submit(&src_buffer, &dst_buffer, &mut buffer_ops);
6873
            assert_eq!(buffer_ops.args_buffer.len(), 1);
6874
        }
6875
        buffer_ops.end_frame(&device, &render_queue);
6876

6877
        // However, both the source and destination need to be contiguous at the same
6878
        // time. Here they are mixed so we can't batch.
6879
        buffer_ops.begin_frame();
6880
        {
6881
            let mut q = InitFillDispatchQueue::default();
6882
            q.enqueue(0, 1);
6883
            assert_eq!(q.queue.len(), 1);
6884
            q.enqueue(1, 0);
6885
            // Ops are not batched yet
6886
            assert_eq!(q.queue.len(), 2);
6887
            // On submit, the ops cannot get batched together
6888
            q.submit(&src_buffer, &dst_buffer, &mut buffer_ops);
6889
            assert_eq!(buffer_ops.args_buffer.len(), 2);
6890
        }
6891
        buffer_ops.end_frame(&device, &render_queue);
6892
    }
6893
}
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