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

djeedai / bevy_hanabi / 14602822889

22 Apr 2025 07:17PM UTC coverage: 39.892%. Remained the same
14602822889

push

github

web-flow
Remove unused `GpuEffectMetadata::spawner_index` field. (#459)

1 of 6 new or added lines in 2 files covered. (16.67%)

2 existing lines in 1 file now uncovered.

3041 of 7623 relevant lines covered (39.89%)

17.6 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 naga_oil::compose::{Composer, NagaModuleDescriptor};
60

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

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

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

96
use self::batch::EffectBatch;
97

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1294
            let shader_defs = default();
×
1295

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

2722
        trace!("Adding {} newly spawned effects", added_effects.len());
×
2723
        for added_effect in added_effects.drain(..) {
×
2724
            trace!("+ added effect: capacity={}", added_effect.capacity);
×
2725

2726
            // Allocate an indirect dispatch arguments struct for this instance
2727
            let update_dispatch_indirect_buffer_table_id = self
2728
                .update_dispatch_indirect_buffer
2729
                .insert(GpuDispatchIndirect::default());
2730

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

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

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

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

2852
            // Allocate storage for properties if needed
2853
            if !added_effect.property_layout.is_empty() {
×
2854
                let cached_effect_properties = property_cache.insert(&added_effect.property_layout);
×
2855
                cmd.insert(cached_effect_properties);
×
2856
            } else {
2857
                cmd.remove::<CachedEffectProperties>();
×
2858
            }
2859

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

2876
            // Allocate storage for GPU spawn events if needed
2877
            if let Some(parent) = added_effect.parent.as_ref() {
×
2878
                let cached_events = event_cache.allocate(parent.event_count);
2879
                cmd.insert(cached_events);
2880
            } else {
2881
                cmd.remove::<CachedEffectEvents>();
×
2882
            }
2883

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

2896
            // Ensure the metadata@3 bind group layout exists for init pass.
2897
            {
2898
                let consume_gpu_spawn_events = added_effect
2899
                    .layout_flags
2900
                    .contains(LayoutFlags::CONSUME_GPU_SPAWN_EVENTS);
2901
                effect_cache.ensure_metadata_init_bind_group_layout(consume_gpu_spawn_events);
2902
            }
2903

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

2910
            trace!(
2911
                "+ added effect entity {:?}: main_entity={:?} \
×
2912
                first_update_group_dispatch_buffer_index={} \
×
2913
                render_effect_dispatch_buffer_id={}",
×
2914
                added_effect.render_entity,
2915
                added_effect.entity,
2916
                update_dispatch_indirect_buffer_table_id.0,
2917
                effect_metadata_buffer_table_id.0
2918
            );
2919
        }
2920

2921
        // Once all changes are applied, immediately schedule any GPU buffer
2922
        // (re)allocation based on the new buffer size. The actual GPU buffer content
2923
        // will be written later.
2924
        if self
×
2925
            .update_dispatch_indirect_buffer
×
2926
            .allocate_gpu(render_device, render_queue)
×
2927
        {
2928
            // All those bind groups use the buffer so need to be re-created
2929
            trace!("*** Dispatch indirect buffer for update pass re-allocated; clearing all bind groups using it.");
×
2930
            effect_bind_groups.particle_buffers.clear();
×
2931
        }
2932
    }
2933

2934
    pub fn allocate_spawner(
×
2935
        &mut self,
2936
        global_transform: &GlobalTransform,
2937
        spawn_count: u32,
2938
        prng_seed: u32,
2939
        effect_metadata_buffer_table_id: BufferTableId,
2940
    ) -> u32 {
2941
        let spawner_base = self.spawner_buffer.len() as u32;
×
2942
        let transform = global_transform.compute_matrix().into();
×
2943
        let inverse_transform = Mat4::from(
2944
            // Inverse the Affine3A first, then convert to Mat4. This is a lot more
2945
            // efficient than inversing the Mat4.
2946
            global_transform.affine().inverse(),
×
2947
        )
2948
        .into();
2949
        let spawner_params = GpuSpawnerParams {
2950
            transform,
2951
            inverse_transform,
2952
            spawn: spawn_count as i32,
×
2953
            seed: prng_seed,
2954
            effect_metadata_index: effect_metadata_buffer_table_id.0,
×
2955
            ..default()
2956
        };
2957
        trace!("spawner params = {:?}", spawner_params);
×
2958
        self.spawner_buffer.push(spawner_params);
×
2959
        spawner_base
×
2960
    }
2961
}
2962

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

2998
impl Default for LayoutFlags {
2999
    fn default() -> Self {
1✔
3000
        Self::NONE
1✔
3001
    }
3002
}
3003

3004
/// Observer raised when the [`CachedEffect`] component is removed, which
3005
/// indicates that the effect instance was despawned.
3006
pub(crate) fn on_remove_cached_effect(
×
3007
    trigger: Trigger<OnRemove, CachedEffect>,
3008
    query: Query<(
3009
        Entity,
3010
        MainEntity,
3011
        &CachedEffect,
3012
        &DispatchBufferIndices,
3013
        Option<&CachedEffectProperties>,
3014
        Option<&CachedParentInfo>,
3015
        Option<&CachedEffectEvents>,
3016
    )>,
3017
    mut effect_cache: ResMut<EffectCache>,
3018
    mut effect_bind_groups: ResMut<EffectBindGroups>,
3019
    mut effects_meta: ResMut<EffectsMeta>,
3020
    mut event_cache: ResMut<EventCache>,
3021
) {
3022
    #[cfg(feature = "trace")]
3023
    let _span = bevy::utils::tracing::info_span!("on_remove_cached_effect").entered();
×
3024

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

3028
    // Fecth the components of the effect being destroyed. Note that the despawn
3029
    // command above is not yet applied, so this query should always succeed.
3030
    let Ok((
3031
        render_entity,
×
3032
        main_entity,
×
3033
        cached_effect,
×
3034
        dispatch_buffer_indices,
×
3035
        _opt_props,
×
3036
        _opt_parent,
×
3037
        opt_cached_effect_events,
×
3038
    )) = query.get(trigger.entity())
×
3039
    else {
3040
        return;
×
3041
    };
3042

3043
    // Dealllocate the effect slice in the event buffer, if any.
3044
    if let Some(cached_effect_events) = opt_cached_effect_events {
×
3045
        match event_cache.free(cached_effect_events) {
3046
            Err(err) => {
×
3047
                error!("Error while freeing effect event slice: {err:?}");
×
3048
            }
3049
            Ok(buffer_state) => {
×
3050
                if buffer_state != BufferState::Used {
×
3051
                    // Clear bind groups associated with the old buffer
3052
                    effect_bind_groups.init_metadata_bind_groups.clear();
×
3053
                    effect_bind_groups.update_metadata_bind_groups.clear();
×
3054
                }
3055
            }
3056
        }
3057
    }
3058

3059
    // Deallocate the effect slice in the GPU effect buffer, and if this was the
3060
    // last slice, also deallocate the GPU buffer itself.
3061
    trace!(
3062
        "=> ParticleEffect on render entity {:?} associated with main entity {:?}, removing...",
×
3063
        render_entity,
3064
        main_entity,
3065
    );
3066
    let Ok(BufferState::Free) = effect_cache.remove(cached_effect) else {
3067
        // Buffer was not affected, so all bind groups are still valid. Nothing else to
3068
        // do.
3069
        return;
×
3070
    };
3071

3072
    // Clear bind groups associated with the removed buffer
3073
    trace!(
×
3074
        "=> GPU buffer #{} gone, destroying its bind groups...",
×
3075
        cached_effect.buffer_index
3076
    );
3077
    effect_bind_groups
3078
        .particle_buffers
3079
        .remove(&cached_effect.buffer_index);
3080
    effects_meta
3081
        .update_dispatch_indirect_buffer
3082
        .remove(dispatch_buffer_indices.update_dispatch_indirect_buffer_table_id);
3083
    effects_meta
3084
        .effect_metadata_buffer
3085
        .remove(dispatch_buffer_indices.effect_metadata_buffer_table_id);
3086
}
3087

3088
/// Update the [`CachedEffect`] component for any newly allocated effect.
3089
///
3090
/// After this system ran, and its commands are applied, all valid extracted
3091
/// effects have a corresponding entity in the render world, with a
3092
/// [`CachedEffect`] component. From there, we operate on those exclusively.
3093
pub(crate) fn add_effects(
×
3094
    render_device: Res<RenderDevice>,
3095
    render_queue: Res<RenderQueue>,
3096
    mesh_allocator: Res<MeshAllocator>,
3097
    render_meshes: Res<RenderAssets<RenderMesh>>,
3098
    commands: Commands,
3099
    mut effects_meta: ResMut<EffectsMeta>,
3100
    mut effect_cache: ResMut<EffectCache>,
3101
    mut property_cache: ResMut<PropertyCache>,
3102
    mut event_cache: ResMut<EventCache>,
3103
    mut extracted_effects: ResMut<ExtractedEffects>,
3104
    mut effect_bind_groups: ResMut<EffectBindGroups>,
3105
    mut sort_bind_groups: ResMut<SortBindGroups>,
3106
) {
3107
    #[cfg(feature = "trace")]
3108
    let _span = bevy::utils::tracing::info_span!("add_effects").entered();
×
3109
    trace!("add_effects");
×
3110

3111
    // Clear last frame's buffer resizes which may have occured during last frame,
3112
    // during `Node::run()` while the `BufferTable` could not be mutated. This is
3113
    // the first point at which we can do that where we're not blocking the main
3114
    // world (so, excluding the extract system).
3115
    effects_meta
×
3116
        .update_dispatch_indirect_buffer
×
3117
        .clear_previous_frame_resizes();
3118
    effects_meta
×
3119
        .effect_metadata_buffer
×
3120
        .clear_previous_frame_resizes();
3121
    sort_bind_groups.clear_previous_frame_resizes();
×
3122

3123
    // Allocate new effects
3124
    effects_meta.add_effects(
×
3125
        commands,
×
3126
        std::mem::take(&mut extracted_effects.added_effects),
×
3127
        &render_device,
×
3128
        &render_queue,
×
3129
        &mesh_allocator,
×
3130
        &render_meshes,
×
3131
        &mut effect_bind_groups,
×
3132
        &mut effect_cache,
×
3133
        &mut property_cache,
×
3134
        &mut event_cache,
×
3135
    );
3136

3137
    // Note: we don't need to explicitly allocate GPU buffers for effects,
3138
    // because EffectBuffer already contains a reference to the
3139
    // RenderDevice, so has done so internally. This is not ideal
3140
    // design-wise, but works.
3141
}
3142

3143
/// Check if two lists of entities are equal.
3144
fn is_child_list_changed(
×
3145
    parent_entity: Entity,
3146
    old: impl ExactSizeIterator<Item = Entity>,
3147
    new: impl ExactSizeIterator<Item = Entity>,
3148
) -> bool {
3149
    if old.len() != new.len() {
×
3150
        trace!(
×
3151
            "Child list changed for effect {:?}: old #{} != new #{}",
×
3152
            parent_entity,
×
3153
            old.len(),
×
3154
            new.len()
×
3155
        );
3156
        return true;
×
3157
    }
3158

3159
    // TODO - this value is arbitrary
3160
    if old.len() >= 16 {
×
3161
        // For large-ish lists, use a hash set.
3162
        let old = HashSet::from_iter(old);
×
3163
        let new = HashSet::from_iter(new);
×
3164
        if old != new {
×
3165
            trace!(
×
3166
                "Child list changed for effect {parent_entity:?}: old [{old:?}] != new [{new:?}]"
×
3167
            );
3168
            true
×
3169
        } else {
3170
            false
×
3171
        }
3172
    } else {
3173
        // For small lists, just use a linear array and sort it
3174
        let mut old = old.collect::<Vec<_>>();
×
3175
        let mut new = new.collect::<Vec<_>>();
×
3176
        old.sort_unstable();
×
3177
        new.sort_unstable();
×
3178
        if old != new {
×
3179
            trace!(
×
3180
                "Child list changed for effect {parent_entity:?}: old [{old:?}] != new [{new:?}]"
×
3181
            );
3182
            true
×
3183
        } else {
3184
            false
×
3185
        }
3186
    }
3187
}
3188

3189
/// Resolve parents and children, updating their [`CachedParent`] and
3190
/// [`CachedChild`] components, as well as (re-)allocating any [`GpuChildInfo`]
3191
/// slice for all children of each parent.
3192
pub(crate) fn resolve_parents(
×
3193
    mut commands: Commands,
3194
    q_child_effects: Query<
3195
        (
3196
            Entity,
3197
            &CachedParentRef,
3198
            &CachedEffectEvents,
3199
            Option<&CachedChildInfo>,
3200
        ),
3201
        With<CachedEffect>,
3202
    >,
3203
    q_cached_effects: Query<(Entity, MainEntity, &CachedEffect)>,
3204
    effect_cache: Res<EffectCache>,
3205
    mut q_parent_effects: Query<(Entity, &mut CachedParentInfo), With<CachedEffect>>,
3206
    mut event_cache: ResMut<EventCache>,
3207
    mut children_from_parent: Local<
3208
        HashMap<Entity, (Vec<(Entity, BufferBindingSource)>, Vec<GpuChildInfo>)>,
3209
    >,
3210
) {
3211
    #[cfg(feature = "trace")]
3212
    let _span = bevy::utils::tracing::info_span!("resolve_parents").entered();
×
3213
    let num_parent_effects = q_parent_effects.iter().len();
×
3214
    trace!("resolve_parents: num_parents={num_parent_effects}");
×
3215

3216
    // Build map of render entity from main entity for all cached effects.
3217
    let render_from_main_entity = q_cached_effects
×
3218
        .iter()
3219
        .map(|(render_entity, main_entity, _)| (main_entity, render_entity))
×
3220
        .collect::<HashMap<_, _>>();
3221

3222
    // Record all parents with children that changed so that we can mark those
3223
    // parents' `CachedParentInfo` as changed. See the comment in the
3224
    // `q_parent_effects` loop for more information.
3225
    let mut parents_with_dirty_children = EntityHashSet::default();
×
3226

3227
    // Group child effects by parent, building a list of children for each parent,
3228
    // solely based on the declaration each child makes of its parent. This doesn't
3229
    // mean yet that the parent exists.
3230
    if children_from_parent.capacity() < num_parent_effects {
×
3231
        let extra = num_parent_effects - children_from_parent.capacity();
×
3232
        children_from_parent.reserve(extra);
×
3233
    }
3234
    for (child_entity, cached_parent_ref, cached_effect_events, cached_child_info) in
×
3235
        q_child_effects.iter()
×
3236
    {
3237
        // Resolve the parent reference into the render world
3238
        let parent_main_entity = cached_parent_ref.entity;
3239
        let Some(parent_entity) = render_from_main_entity.get(&parent_main_entity.id()) else {
×
3240
            warn!(
×
3241
                "Cannot resolve parent render entity for parent main entity {:?}, removing CachedChildInfo from child entity {:?}.",
×
3242
                parent_main_entity, child_entity
3243
            );
3244
            commands.entity(child_entity).remove::<CachedChildInfo>();
×
3245
            continue;
×
3246
        };
3247
        let parent_entity = *parent_entity;
3248

3249
        // Resolve the parent
3250
        let Ok((_, _, parent_cached_effect)) = q_cached_effects.get(parent_entity) else {
×
3251
            // Since we failed to resolve, remove this component so the next systems ignore
3252
            // this effect.
3253
            warn!(
×
3254
                "Unknown parent render entity {:?}, removing CachedChildInfo from child entity {:?}.",
×
3255
                parent_entity, child_entity
3256
            );
3257
            commands.entity(child_entity).remove::<CachedChildInfo>();
×
3258
            continue;
×
3259
        };
3260
        let Some(parent_buffer_binding_source) = effect_cache
×
3261
            .get_buffer(parent_cached_effect.buffer_index)
3262
            .map(|effect_buffer| effect_buffer.max_binding_source())
×
3263
        else {
3264
            // Since we failed to resolve, remove this component so the next systems ignore
3265
            // this effect.
3266
            warn!(
×
3267
                "Unknown parent buffer #{} on entity {:?}, removing CachedChildInfo.",
×
3268
                parent_cached_effect.buffer_index, child_entity
3269
            );
3270
            commands.entity(child_entity).remove::<CachedChildInfo>();
×
3271
            continue;
×
3272
        };
3273

3274
        let Some(child_event_buffer) = event_cache.get_buffer(cached_effect_events.buffer_index)
×
3275
        else {
3276
            // Since we failed to resolve, remove this component so the next systems ignore
3277
            // this effect.
3278
            warn!(
×
3279
                "Unknown child event buffer #{} on entity {:?}, removing CachedChildInfo.",
×
3280
                cached_effect_events.buffer_index, child_entity
3281
            );
3282
            commands.entity(child_entity).remove::<CachedChildInfo>();
×
3283
            continue;
×
3284
        };
3285
        let child_buffer_binding_source = BufferBindingSource {
3286
            buffer: child_event_buffer.clone(),
3287
            offset: cached_effect_events.range.start,
3288
            size: NonZeroU32::new(cached_effect_events.range.len() as u32).unwrap(),
3289
        };
3290

3291
        // Push the child entity into the children list
3292
        let (child_vec, child_infos) = children_from_parent.entry(parent_entity).or_default();
3293
        let local_child_index = child_vec.len() as u32;
3294
        child_vec.push((child_entity, child_buffer_binding_source));
3295
        child_infos.push(GpuChildInfo {
3296
            event_count: 0,
3297
            init_indirect_dispatch_index: cached_effect_events.init_indirect_dispatch_index,
3298
        });
3299

3300
        // Check if child info changed. Avoid overwriting if no change.
3301
        if let Some(old_cached_child_info) = cached_child_info {
×
3302
            if parent_entity == old_cached_child_info.parent
3303
                && parent_cached_effect.slice.particle_layout
×
3304
                    == old_cached_child_info.parent_particle_layout
×
3305
                && parent_buffer_binding_source
×
3306
                    == old_cached_child_info.parent_buffer_binding_source
×
3307
                // 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.
3308
                && local_child_index == old_cached_child_info.local_child_index
×
3309
                && cached_effect_events.init_indirect_dispatch_index
×
3310
                    == old_cached_child_info.init_indirect_dispatch_index
×
3311
            {
3312
                trace!(
×
3313
                    "ChildInfo didn't change for child entity {:?}, skipping component write.",
×
3314
                    child_entity
3315
                );
3316
                continue;
×
3317
            }
3318
        }
3319

3320
        // Allocate (or overwrite, if already existing) the child info, now that the
3321
        // parent is resolved.
3322
        let cached_child_info = CachedChildInfo {
3323
            parent: parent_entity,
3324
            parent_particle_layout: parent_cached_effect.slice.particle_layout.clone(),
×
3325
            parent_buffer_binding_source,
3326
            local_child_index,
3327
            global_child_index: u32::MAX, // fixed up later by fixup_parents()
3328
            init_indirect_dispatch_index: cached_effect_events.init_indirect_dispatch_index,
×
3329
        };
3330
        commands.entity(child_entity).insert(cached_child_info);
×
3331
        trace!("Spawned CachedChildInfo on child entity {:?}", child_entity);
×
3332

3333
        // Make a note of the parent entity so that we remember to mark its
3334
        // `CachedParentInfo` as changed below.
3335
        parents_with_dirty_children.insert(parent_entity);
3336
    }
3337

3338
    // Once all parents are resolved, diff all children of already-cached parents,
3339
    // and re-allocate their GpuChildInfo if needed.
3340
    for (parent_entity, mut cached_parent_info) in q_parent_effects.iter_mut() {
×
3341
        // Fetch the newly extracted list of children
3342
        let Some((_, (children, child_infos))) = children_from_parent.remove_entry(&parent_entity)
×
3343
        else {
3344
            trace!("Entity {parent_entity:?} is no more a parent, removing CachedParentInfo component...");
×
3345
            commands.entity(parent_entity).remove::<CachedParentInfo>();
×
3346
            continue;
×
3347
        };
3348

3349
        // If we updated `CachedChildInfo` for any of this entity's children,
3350
        // then even if the check below passes, we must still set the change
3351
        // flag on this entity's `CachedParentInfo`. That's because the
3352
        // `fixup_parents` system looks at the change flag for the parent in
3353
        // order to determine which `CachedChildInfo` it needs to update, and
3354
        // that system must process all newly-added `CachedChildInfo`s.
3355
        if parents_with_dirty_children.contains(&parent_entity) {
×
3356
            cached_parent_info.set_changed();
×
3357
        }
3358

3359
        // Check if any child changed compared to the existing CachedChildren component
3360
        if !is_child_list_changed(
3361
            parent_entity,
3362
            cached_parent_info
3363
                .children
3364
                .iter()
3365
                .map(|(entity, _)| *entity),
×
3366
            children.iter().map(|(entity, _)| *entity),
×
3367
        ) {
3368
            continue;
×
3369
        }
3370

3371
        event_cache.reallocate_child_infos(
×
3372
            parent_entity,
×
3373
            children,
×
3374
            &child_infos[..],
×
3375
            cached_parent_info.deref_mut(),
×
3376
        );
3377
    }
3378

3379
    // Once this is done, the children hash map contains all entries which don't
3380
    // already have a CachedParentInfo component. That is, all entities which are
3381
    // new parents.
3382
    for (parent_entity, (children, child_infos)) in children_from_parent.drain() {
×
3383
        let cached_parent_info =
3384
            event_cache.allocate_child_infos(parent_entity, children, &child_infos[..]);
3385
        commands.entity(parent_entity).insert(cached_parent_info);
3386
    }
3387

3388
    // // Once all changes are applied, immediately schedule any GPU buffer
3389
    // // (re)allocation based on the new buffer size. The actual GPU buffer
3390
    // content // will be written later.
3391
    // if event_cache
3392
    //     .child_infos()
3393
    //     .allocate_gpu(render_device, render_queue)
3394
    // {
3395
    //     // All those bind groups use the buffer so need to be re-created
3396
    //     effect_bind_groups.particle_buffers.clear();
3397
    // }
3398
}
3399

3400
pub fn fixup_parents(
×
3401
    q_changed_parents: Query<(Entity, &CachedParentInfo), Changed<CachedParentInfo>>,
3402
    mut q_children: Query<&mut CachedChildInfo>,
3403
) {
3404
    #[cfg(feature = "trace")]
3405
    let _span = bevy::utils::tracing::info_span!("fixup_parents").entered();
×
3406
    trace!("fixup_parents");
×
3407

3408
    // Once all parents are (re-)allocated, fix up the global index of all
3409
    // children if the parent base index changed.
3410
    trace!(
×
3411
        "Updating the global index of children of parent effects whose child list just changed..."
×
3412
    );
3413
    for (parent_entity, cached_parent_info) in q_changed_parents.iter() {
×
3414
        let base_index =
3415
            cached_parent_info.byte_range.start / GpuChildInfo::SHADER_SIZE.get() as u32;
3416
        trace!(
3417
            "Updating {} children of parent effect {:?} with base child index {}...",
×
3418
            cached_parent_info.children.len(),
×
3419
            parent_entity,
3420
            base_index
3421
        );
3422
        for (child_entity, _) in &cached_parent_info.children {
×
3423
            let Ok(mut cached_child_info) = q_children.get_mut(*child_entity) else {
×
3424
                continue;
×
3425
            };
3426
            cached_child_info.global_child_index = base_index + cached_child_info.local_child_index;
×
3427
            trace!(
×
3428
                "+ Updated global index for child ID {:?} of parent {:?}: local={}, global={}",
×
3429
                child_entity,
×
3430
                parent_entity,
×
3431
                cached_child_info.local_child_index,
×
3432
                cached_child_info.global_child_index
×
3433
            );
3434
        }
3435
    }
3436
}
3437

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

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

3464
/// Render world cached mesh infos for a single effect instance.
3465
#[derive(Debug, Clone, Component)]
3466
pub(crate) struct CachedMesh {
3467
    /// Asset of the effect mesh to draw.
3468
    pub mesh: AssetId<Mesh>,
3469
    /// GPU buffer storing the [`mesh`] of the effect.
3470
    pub buffer: Buffer,
3471
    /// Range slice inside the GPU buffer for the effect mesh.
3472
    pub range: Range<u32>,
3473
    /// Indexed rendering metadata.
3474
    #[allow(unused)]
3475
    pub indexed: Option<MeshIndexSlice>,
3476
}
3477

3478
/// Render world cached properties info for a single effect instance.
3479
#[allow(unused)]
3480
#[derive(Debug, Component)]
3481
pub(crate) struct CachedProperties {
3482
    /// Layout of the effect properties.
3483
    pub layout: PropertyLayout,
3484
    /// Index of the buffer in the [`EffectCache`].
3485
    pub buffer_index: u32,
3486
    /// Offset in bytes inside the buffer.
3487
    pub offset: u32,
3488
    /// Binding size in bytes of the property struct.
3489
    pub binding_size: u32,
3490
}
3491

3492
#[derive(SystemParam)]
3493
pub struct PrepareEffectsReadOnlyParams<'w, 's> {
3494
    sim_params: Res<'w, SimParams>,
3495
    render_device: Res<'w, RenderDevice>,
3496
    render_queue: Res<'w, RenderQueue>,
3497
    #[system_param(ignore)]
3498
    marker: PhantomData<&'s usize>,
3499
}
3500

3501
#[derive(SystemParam)]
3502
pub struct PipelineSystemParams<'w, 's> {
3503
    pipeline_cache: Res<'w, PipelineCache>,
3504
    init_pipeline: ResMut<'w, ParticlesInitPipeline>,
3505
    indirect_pipeline: Res<'w, DispatchIndirectPipeline>,
3506
    update_pipeline: ResMut<'w, ParticlesUpdatePipeline>,
3507
    specialized_init_pipelines: ResMut<'w, SpecializedComputePipelines<ParticlesInitPipeline>>,
3508
    specialized_update_pipelines: ResMut<'w, SpecializedComputePipelines<ParticlesUpdatePipeline>>,
3509
    specialized_indirect_pipelines:
3510
        ResMut<'w, SpecializedComputePipelines<DispatchIndirectPipeline>>,
3511
    #[system_param(ignore)]
3512
    marker: PhantomData<&'s usize>,
3513
}
3514

3515
pub(crate) fn prepare_effects(
×
3516
    mut commands: Commands,
3517
    read_only_params: PrepareEffectsReadOnlyParams,
3518
    mut pipelines: PipelineSystemParams,
3519
    mut property_cache: ResMut<PropertyCache>,
3520
    event_cache: Res<EventCache>,
3521
    mut effect_cache: ResMut<EffectCache>,
3522
    mut effects_meta: ResMut<EffectsMeta>,
3523
    mut effect_bind_groups: ResMut<EffectBindGroups>,
3524
    mut extracted_effects: ResMut<ExtractedEffects>,
3525
    mut property_bind_groups: ResMut<PropertyBindGroups>,
3526
    q_cached_effects: Query<(
3527
        MainEntity,
3528
        &CachedEffect,
3529
        Ref<CachedMesh>,
3530
        &DispatchBufferIndices,
3531
        Option<&CachedEffectProperties>,
3532
        Option<&CachedParentInfo>,
3533
        Option<&CachedChildInfo>,
3534
        Option<&CachedEffectEvents>,
3535
    )>,
3536
    q_debug_all_entities: Query<MainEntity>,
3537
    mut gpu_buffer_operations: ResMut<GpuBufferOperations>,
3538
    mut sort_bind_groups: ResMut<SortBindGroups>,
3539
    mut init_fill_dispatch_queue: ResMut<InitFillDispatchQueue>,
3540
) {
3541
    #[cfg(feature = "trace")]
3542
    let _span = bevy::utils::tracing::info_span!("prepare_effects").entered();
×
3543
    trace!("prepare_effects");
×
3544

3545
    init_fill_dispatch_queue.clear();
×
3546

3547
    // Workaround for too many params in system (TODO: refactor to split work?)
3548
    let sim_params = read_only_params.sim_params.into_inner();
×
3549
    let render_device = read_only_params.render_device.into_inner();
×
3550
    let render_queue = read_only_params.render_queue.into_inner();
×
3551
    let pipeline_cache = pipelines.pipeline_cache.into_inner();
×
3552
    let specialized_init_pipelines = pipelines.specialized_init_pipelines.into_inner();
×
3553
    let specialized_update_pipelines = pipelines.specialized_update_pipelines.into_inner();
×
3554
    let specialized_indirect_pipelines = pipelines.specialized_indirect_pipelines.into_inner();
×
3555

3556
    // // sort first by z and then by handle. this ensures that, when possible,
3557
    // batches span multiple z layers // batches won't span z-layers if there is
3558
    // another batch between them extracted_effects.effects.sort_by(|a, b| {
3559
    //     match FloatOrd(a.transform.w_axis[2]).cmp(&FloatOrd(b.transform.
3560
    // w_axis[2])) {         Ordering::Equal => a.handle.cmp(&b.handle),
3561
    //         other => other,
3562
    //     }
3563
    // });
3564

3565
    // Ensure the indirect pipelines are created
3566
    if effects_meta.indirect_pipeline_ids[0] == CachedComputePipelineId::INVALID {
×
3567
        effects_meta.indirect_pipeline_ids[0] = specialized_indirect_pipelines.specialize(
×
3568
            pipeline_cache,
×
3569
            &pipelines.indirect_pipeline,
×
3570
            DispatchIndirectPipelineKey { has_events: false },
×
3571
        );
3572
    }
3573
    if effects_meta.indirect_pipeline_ids[1] == CachedComputePipelineId::INVALID {
×
3574
        effects_meta.indirect_pipeline_ids[1] = specialized_indirect_pipelines.specialize(
×
3575
            pipeline_cache,
×
3576
            &pipelines.indirect_pipeline,
×
3577
            DispatchIndirectPipelineKey { has_events: true },
×
3578
        );
3579
    }
3580
    if effects_meta.active_indirect_pipeline_id == CachedComputePipelineId::INVALID {
×
3581
        effects_meta.active_indirect_pipeline_id = effects_meta.indirect_pipeline_ids[0];
×
3582
    } else {
3583
        // If this is the first time we insert an event buffer, we need to switch the
3584
        // indirect pass from non-event to event mode. That is, we need to re-allocate
3585
        // the pipeline with the child infos buffer binding. Conversely, if there's no
3586
        // more effect using GPU spawn events, we can deallocate.
3587
        let was_empty =
×
3588
            effects_meta.active_indirect_pipeline_id == effects_meta.indirect_pipeline_ids[0];
×
3589
        let is_empty = event_cache.child_infos().is_empty();
×
3590
        if was_empty && !is_empty {
×
3591
            trace!("First event buffer inserted; switching indirect pass to event mode...");
×
3592
            effects_meta.active_indirect_pipeline_id = effects_meta.indirect_pipeline_ids[1];
×
3593
        } else if is_empty && !was_empty {
×
3594
            trace!("Last event buffer removed; switching indirect pass to no-event mode...");
×
3595
            effects_meta.active_indirect_pipeline_id = effects_meta.indirect_pipeline_ids[0];
×
3596
        }
3597
    }
3598

3599
    gpu_buffer_operations.begin_frame();
×
3600

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

3604
    // Build batcher inputs from extracted effects, updating all cached components
3605
    // for each effect on the fly.
3606
    let effects = std::mem::take(&mut extracted_effects.effects);
×
3607
    let extracted_effect_count = effects.len();
×
3608
    let mut prepared_effect_count = 0;
×
3609
    for extracted_effect in effects.into_iter() {
×
3610
        // Skip effects not cached. Since we're iterating over the extracted effects
3611
        // instead of the cached ones, it might happen we didn't cache some effect on
3612
        // purpose because they failed earlier validations.
3613
        // FIXME - extract into ECS directly so we don't have to do that?
3614
        let Ok((
3615
            main_entity,
×
3616
            cached_effect,
×
3617
            cached_mesh,
×
3618
            dispatch_buffer_indices,
×
3619
            cached_effect_properties,
×
3620
            cached_parent_info,
×
3621
            cached_child_info,
×
3622
            cached_effect_events,
×
3623
        )) = q_cached_effects.get(extracted_effect.render_entity.id())
×
3624
        else {
3625
            warn!(
×
3626
                "Unknown render entity {:?} for extracted effect.",
×
3627
                extracted_effect.render_entity.id()
×
3628
            );
3629
            if let Ok(main_entity) = q_debug_all_entities.get(extracted_effect.render_entity.id()) {
×
3630
                info!(
3631
                    "Render entity {:?} exists with main entity {:?}, some component missing!",
×
3632
                    extracted_effect.render_entity.id(),
×
3633
                    main_entity
3634
                );
3635
            } else {
3636
                info!(
×
3637
                    "Render entity {:?} does not exists with a MainEntity.",
×
3638
                    extracted_effect.render_entity.id()
×
3639
                );
3640
            }
3641
            continue;
×
3642
        };
3643

3644
        let effect_slice = EffectSlice {
3645
            slice: cached_effect.slice.range(),
3646
            buffer_index: cached_effect.buffer_index,
3647
            particle_layout: cached_effect.slice.particle_layout.clone(),
3648
        };
3649

3650
        let has_event_buffer = cached_child_info.is_some();
3651
        // FIXME: decouple "consumes event" from "reads parent particle" (here, p.layout
3652
        // should be Option<T>, not T)
3653
        let property_layout_min_binding_size = if extracted_effect.property_layout.is_empty() {
3654
            None
×
3655
        } else {
3656
            Some(extracted_effect.property_layout.min_binding_size())
×
3657
        };
3658

3659
        // Schedule some GPU buffer operation to update the number of workgroups to
3660
        // dispatch during the indirect init pass of this effect based on the number of
3661
        // GPU spawn events written in its buffer.
3662
        if let (Some(cached_effect_events), Some(cached_child_info)) =
×
3663
            (cached_effect_events, cached_child_info)
3664
        {
3665
            debug_assert_eq!(
3666
                GpuChildInfo::min_size().get() % 4,
3667
                0,
3668
                "Invalid GpuChildInfo alignment."
×
3669
            );
3670

3671
            // Resolve parent entry
3672
            let Ok((_, _, _, _, _, cached_parent_info, _, _)) =
×
3673
                q_cached_effects.get(cached_child_info.parent)
×
3674
            else {
3675
                continue;
×
3676
            };
3677
            let Some(cached_parent_info) = cached_parent_info else {
×
3678
                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);
×
3679
                continue;
×
3680
            };
3681

3682
            let init_indirect_dispatch_index = cached_effect_events.init_indirect_dispatch_index;
3683
            assert_eq!(0, cached_parent_info.byte_range.start % 4);
3684
            let global_child_index = cached_child_info.global_child_index;
×
3685

3686
            // Schedule a fill dispatch
3687
            trace!(
×
3688
                "init_fill_dispatch.push(): src:global_child_index={} dst:init_indirect_dispatch_index={}",
×
3689
                global_child_index,
3690
                init_indirect_dispatch_index,
3691
            );
3692
            init_fill_dispatch_queue.enqueue(global_child_index, init_indirect_dispatch_index);
×
3693
        }
3694

3695
        // Create init pipeline key flags.
3696
        let init_pipeline_key_flags = {
×
3697
            let mut flags = ParticleInitPipelineKeyFlags::empty();
×
3698
            flags.set(
×
3699
                ParticleInitPipelineKeyFlags::ATTRIBUTE_PREV,
×
3700
                effect_slice.particle_layout.contains(Attribute::PREV),
×
3701
            );
3702
            flags.set(
×
3703
                ParticleInitPipelineKeyFlags::ATTRIBUTE_NEXT,
×
3704
                effect_slice.particle_layout.contains(Attribute::NEXT),
×
3705
            );
3706
            flags.set(
×
3707
                ParticleInitPipelineKeyFlags::CONSUME_GPU_SPAWN_EVENTS,
×
3708
                has_event_buffer,
×
3709
            );
3710
            flags
×
3711
        };
3712

3713
        // This should always exist by the time we reach this point, because we should
3714
        // have inserted any property in the cache, which would have allocated the
3715
        // proper bind group layout (or the default no-property one).
3716
        let spawner_bind_group_layout = property_cache
×
3717
            .bind_group_layout(property_layout_min_binding_size)
×
3718
            .unwrap_or_else(|| {
×
3719
                panic!(
×
3720
                    "Failed to find spawner@2 bind group layout for property binding size {:?}",
×
3721
                    property_layout_min_binding_size,
×
3722
                )
3723
            });
3724
        trace!(
3725
            "Retrieved spawner@2 bind group layout {:?} for property binding size {:?}.",
×
3726
            spawner_bind_group_layout.id(),
×
3727
            property_layout_min_binding_size
3728
        );
3729

3730
        // Fetch the bind group layouts from the cache
3731
        trace!("cached_child_info={:?}", cached_child_info);
×
3732
        let (parent_particle_layout_min_binding_size, parent_buffer_index) =
×
3733
            if let Some(cached_child) = cached_child_info.as_ref() {
×
3734
                let Ok((_, parent_cached_effect, _, _, _, _, _, _)) =
×
3735
                    q_cached_effects.get(cached_child.parent)
3736
                else {
3737
                    // At this point we should have discarded invalid effects with a missing parent,
3738
                    // so if the parent is not found this is a bug.
3739
                    error!(
×
3740
                        "Effect main_entity {:?}: parent render entity {:?} not found.",
×
3741
                        main_entity, cached_child.parent
3742
                    );
3743
                    continue;
×
3744
                };
3745
                (
3746
                    Some(
3747
                        parent_cached_effect
3748
                            .slice
3749
                            .particle_layout
3750
                            .min_binding_size32(),
3751
                    ),
3752
                    Some(parent_cached_effect.buffer_index),
3753
                )
3754
            } else {
3755
                (None, None)
×
3756
            };
3757
        let Some(particle_bind_group_layout) = effect_cache.particle_bind_group_layout(
×
3758
            effect_slice.particle_layout.min_binding_size32(),
3759
            parent_particle_layout_min_binding_size,
3760
        ) else {
3761
            error!("Failed to find particle sim bind group @1 for min_binding_size={} parent_min_binding_size={:?}", 
×
3762
            effect_slice.particle_layout.min_binding_size32(), parent_particle_layout_min_binding_size);
×
3763
            continue;
×
3764
        };
3765
        let particle_bind_group_layout = particle_bind_group_layout.clone();
3766
        trace!(
3767
            "Retrieved particle@1 bind group layout {:?} for particle binding size {:?} and parent binding size {:?}.",
×
3768
            particle_bind_group_layout.id(),
×
3769
            effect_slice.particle_layout.min_binding_size32(),
×
3770
            parent_particle_layout_min_binding_size,
3771
        );
3772

3773
        let particle_layout_min_binding_size = effect_slice.particle_layout.min_binding_size32();
3774
        let spawner_bind_group_layout = spawner_bind_group_layout.clone();
3775

3776
        // Specialize the init pipeline based on the effect.
3777
        let init_pipeline_id = {
3778
            let consume_gpu_spawn_events = init_pipeline_key_flags
3779
                .contains(ParticleInitPipelineKeyFlags::CONSUME_GPU_SPAWN_EVENTS);
3780

3781
            // Fetch the metadata@3 bind group layout from the cache
3782
            let metadata_bind_group_layout = effect_cache
3783
                .metadata_init_bind_group_layout(consume_gpu_spawn_events)
3784
                .unwrap()
3785
                .clone();
3786

3787
            // https://github.com/bevyengine/bevy/issues/17132
3788
            let particle_bind_group_layout_id = particle_bind_group_layout.id();
3789
            let spawner_bind_group_layout_id = spawner_bind_group_layout.id();
3790
            let metadata_bind_group_layout_id = metadata_bind_group_layout.id();
3791
            pipelines.init_pipeline.temp_particle_bind_group_layout =
3792
                Some(particle_bind_group_layout.clone());
3793
            pipelines.init_pipeline.temp_spawner_bind_group_layout =
3794
                Some(spawner_bind_group_layout.clone());
3795
            pipelines.init_pipeline.temp_metadata_bind_group_layout =
3796
                Some(metadata_bind_group_layout);
3797
            let init_pipeline_id: CachedComputePipelineId = specialized_init_pipelines.specialize(
3798
                pipeline_cache,
3799
                &pipelines.init_pipeline,
3800
                ParticleInitPipelineKey {
3801
                    shader: extracted_effect.effect_shaders.init.clone(),
3802
                    particle_layout_min_binding_size,
3803
                    parent_particle_layout_min_binding_size,
3804
                    flags: init_pipeline_key_flags,
3805
                    particle_bind_group_layout_id,
3806
                    spawner_bind_group_layout_id,
3807
                    metadata_bind_group_layout_id,
3808
                },
3809
            );
3810
            // keep things tidy; this is just a hack, should not persist
3811
            pipelines.init_pipeline.temp_particle_bind_group_layout = None;
3812
            pipelines.init_pipeline.temp_spawner_bind_group_layout = None;
3813
            pipelines.init_pipeline.temp_metadata_bind_group_layout = None;
3814
            trace!("Init pipeline specialized: id={:?}", init_pipeline_id);
×
3815

3816
            init_pipeline_id
3817
        };
3818

3819
        let update_pipeline_id = {
3820
            let num_event_buffers = cached_parent_info
3821
                .map(|p| p.children.len() as u32)
×
3822
                .unwrap_or_default();
3823

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

3831
            // Fetch the bind group layouts from the cache
3832
            let metadata_bind_group_layout = effect_cache
3833
                .metadata_update_bind_group_layout(num_event_buffers)
3834
                .unwrap()
3835
                .clone();
3836

3837
            // https://github.com/bevyengine/bevy/issues/17132
3838
            let particle_bind_group_layout_id = particle_bind_group_layout.id();
3839
            let spawner_bind_group_layout_id = spawner_bind_group_layout.id();
3840
            let metadata_bind_group_layout_id = metadata_bind_group_layout.id();
3841
            pipelines.update_pipeline.temp_particle_bind_group_layout =
3842
                Some(particle_bind_group_layout);
3843
            pipelines.update_pipeline.temp_spawner_bind_group_layout =
3844
                Some(spawner_bind_group_layout);
3845
            pipelines.update_pipeline.temp_metadata_bind_group_layout =
3846
                Some(metadata_bind_group_layout);
3847
            let update_pipeline_id = specialized_update_pipelines.specialize(
3848
                pipeline_cache,
3849
                &pipelines.update_pipeline,
3850
                ParticleUpdatePipelineKey {
3851
                    shader: extracted_effect.effect_shaders.update.clone(),
3852
                    particle_layout: effect_slice.particle_layout.clone(),
3853
                    parent_particle_layout_min_binding_size,
3854
                    num_event_buffers,
3855
                    particle_bind_group_layout_id,
3856
                    spawner_bind_group_layout_id,
3857
                    metadata_bind_group_layout_id,
3858
                },
3859
            );
3860
            // keep things tidy; this is just a hack, should not persist
3861
            pipelines.update_pipeline.temp_particle_bind_group_layout = None;
3862
            pipelines.update_pipeline.temp_spawner_bind_group_layout = None;
3863
            pipelines.update_pipeline.temp_metadata_bind_group_layout = None;
3864
            trace!("Update pipeline specialized: id={:?}", update_pipeline_id);
×
3865

3866
            update_pipeline_id
3867
        };
3868

3869
        let init_and_update_pipeline_ids = InitAndUpdatePipelineIds {
3870
            init: init_pipeline_id,
3871
            update: update_pipeline_id,
3872
        };
3873

3874
        // For ribbons, which need particle sorting, create a bind group layout for
3875
        // sorting the effect, based on its particle layout.
3876
        if extracted_effect.layout_flags.contains(LayoutFlags::RIBBONS) {
3877
            if let Err(err) = sort_bind_groups.ensure_sort_fill_bind_group_layout(
×
3878
                pipeline_cache,
×
3879
                &extracted_effect.particle_layout,
×
3880
            ) {
3881
                error!(
3882
                    "Failed to create bind group for ribbon effect sorting: {:?}",
×
3883
                    err
3884
                );
3885
                continue;
3886
            }
3887
        }
3888

3889
        // Output some debug info
3890
        trace!("init_shader = {:?}", extracted_effect.effect_shaders.init);
×
3891
        trace!(
3892
            "update_shader = {:?}",
×
3893
            extracted_effect.effect_shaders.update
3894
        );
3895
        trace!(
3896
            "render_shader = {:?}",
×
3897
            extracted_effect.effect_shaders.render
3898
        );
3899
        trace!("layout_flags = {:?}", extracted_effect.layout_flags);
×
3900
        trace!("particle_layout = {:?}", effect_slice.particle_layout);
×
3901

3902
        let spawner_index = effects_meta.allocate_spawner(
3903
            &extracted_effect.transform,
3904
            extracted_effect.spawn_count,
3905
            extracted_effect.prng_seed,
3906
            dispatch_buffer_indices.effect_metadata_buffer_table_id,
3907
        );
3908

3909
        trace!(
3910
            "Updating cached effect at entity {:?}...",
×
3911
            extracted_effect.render_entity.id()
×
3912
        );
3913
        let mut cmd = commands.entity(extracted_effect.render_entity.id());
3914
        cmd.insert(BatchInput {
3915
            handle: extracted_effect.handle,
3916
            entity: extracted_effect.render_entity.id(),
3917
            main_entity: extracted_effect.main_entity,
3918
            effect_slice,
3919
            init_and_update_pipeline_ids,
3920
            parent_buffer_index,
3921
            event_buffer_index: cached_effect_events.map(|cee| cee.buffer_index),
×
3922
            child_effects: cached_parent_info
3923
                .map(|cp| cp.children.clone())
×
3924
                .unwrap_or_default(),
3925
            layout_flags: extracted_effect.layout_flags,
3926
            texture_layout: extracted_effect.texture_layout.clone(),
3927
            textures: extracted_effect.textures.clone(),
3928
            alpha_mode: extracted_effect.alpha_mode,
3929
            particle_layout: extracted_effect.particle_layout.clone(),
3930
            shaders: extracted_effect.effect_shaders,
3931
            spawner_index,
3932
            spawn_count: extracted_effect.spawn_count,
3933
            position: extracted_effect.transform.translation(),
3934
            init_indirect_dispatch_index: cached_child_info
3935
                .map(|cc| cc.init_indirect_dispatch_index),
×
3936
        });
3937

3938
        // Update properties
3939
        if let Some(cached_effect_properties) = cached_effect_properties {
×
3940
            // Because the component is persisted, it may be there from a previous version
3941
            // of the asset. And add_remove_effects() only add new instances or remove old
3942
            // ones, but doesn't update existing ones. Check if it needs to be removed.
3943
            // FIXME - Dedupe with add_remove_effect(), we shouldn't have 2 codepaths doing
3944
            // the same thing at 2 different times.
3945
            if extracted_effect.property_layout.is_empty() {
3946
                trace!(
×
3947
                    "Render entity {:?} had CachedEffectProperties component, but newly extracted property layout is empty. Removing component...",
×
3948
                    extracted_effect.render_entity.id(),
×
3949
                );
3950
                cmd.remove::<CachedEffectProperties>();
×
3951
                // Also remove the other one. FIXME - dedupe those two...
3952
                cmd.remove::<CachedProperties>();
×
3953

3954
                if extracted_effect.property_data.is_some() {
×
3955
                    warn!(
×
3956
                        "Effect on entity {:?} doesn't declare any property in its Module, but some property values were provided. Those values will be discarded.",
×
3957
                        extracted_effect.main_entity.id(),
×
3958
                    );
3959
                }
3960
            } else {
3961
                // Insert a new component or overwrite the existing one
3962
                cmd.insert(CachedProperties {
×
3963
                    layout: extracted_effect.property_layout.clone(),
×
3964
                    buffer_index: cached_effect_properties.buffer_index,
×
3965
                    offset: cached_effect_properties.range.start,
×
3966
                    binding_size: cached_effect_properties.range.len() as u32,
×
3967
                });
3968

3969
                // Write properties for this effect if they were modified.
3970
                // FIXME - This doesn't work with batching!
3971
                if let Some(property_data) = &extracted_effect.property_data {
×
3972
                    trace!(
3973
                    "Properties changed; (re-)uploading to GPU... New data: {} bytes. Capacity: {} bytes.",
×
3974
                    property_data.len(),
×
3975
                    cached_effect_properties.range.len(),
×
3976
                );
3977
                    if property_data.len() <= cached_effect_properties.range.len() {
×
3978
                        let property_buffer = property_cache.buffers_mut()
×
3979
                            [cached_effect_properties.buffer_index as usize]
×
3980
                            .as_mut()
3981
                            .unwrap();
3982
                        property_buffer.write(cached_effect_properties.range.start, property_data);
×
3983
                    } else {
3984
                        error!(
×
3985
                            "Cannot upload properties: existing property slice in property buffer #{} is too small ({} bytes) for the new data ({} bytes).",
×
3986
                            cached_effect_properties.buffer_index,
×
3987
                            cached_effect_properties.range.len(),
×
3988
                            property_data.len()
×
3989
                        );
3990
                    }
3991
                }
3992
            }
3993
        } else {
3994
            // No property on the effect; remove the component
3995
            trace!(
×
3996
                "No CachedEffectProperties on render entity {:?}, remove any CachedProperties component too.",
×
3997
                extracted_effect.render_entity.id()
×
3998
            );
3999
            cmd.remove::<CachedProperties>();
×
4000
        }
4001

4002
        // Now that the effect is entirely prepared and all GPU resources are allocated,
4003
        // update its GpuEffectMetadata with all those infos.
4004
        // FIXME - should do this only when the below changes (not only the mesh), via
4005
        // some invalidation mechanism and ECS change detection.
4006
        if cached_mesh.is_changed() {
4007
            let capacity = cached_effect.slice.len();
×
4008

4009
            // Global and local indices of this effect as a child of another (parent) effect
4010
            let (global_child_index, local_child_index) = cached_child_info
×
4011
                .map(|cci| (cci.global_child_index, cci.local_child_index))
×
4012
                .unwrap_or_default();
4013

4014
            // Base index of all children of this (parent) effect
4015
            let base_child_index = cached_parent_info
×
4016
                .map(|cpi| {
×
4017
                    debug_assert_eq!(
×
4018
                        cpi.byte_range.start % GpuChildInfo::SHADER_SIZE.get() as u32,
×
4019
                        0
4020
                    );
4021
                    cpi.byte_range.start / GpuChildInfo::SHADER_SIZE.get() as u32
×
4022
                })
4023
                .unwrap_or_default();
4024

4025
            let particle_stride = extracted_effect.particle_layout.min_binding_size32().get() / 4;
×
4026
            let sort_key_offset = extracted_effect
×
4027
                .particle_layout
×
4028
                .offset(Attribute::RIBBON_ID)
×
4029
                .unwrap_or(0)
×
4030
                / 4;
×
4031
            let sort_key2_offset = extracted_effect
×
4032
                .particle_layout
×
4033
                .offset(Attribute::AGE)
×
4034
                .unwrap_or(0)
×
4035
                / 4;
×
4036

4037
            let mut gpu_effect_metadata = GpuEffectMetadata {
4038
                instance_count: 0,
4039
                base_instance: 0,
4040
                alive_count: 0,
4041
                max_update: 0,
4042
                dead_count: capacity,
4043
                max_spawn: capacity,
4044
                ping: 0,
UNCOV
4045
                indirect_dispatch_index: dispatch_buffer_indices
×
4046
                    .update_dispatch_indirect_buffer_table_id
4047
                    .0,
4048
                // Note: the indirect draw args are at the start of the GpuEffectMetadata struct
4049
                indirect_render_index: dispatch_buffer_indices.effect_metadata_buffer_table_id.0,
×
4050
                init_indirect_dispatch_index: cached_effect_events
×
4051
                    .map(|cee| cee.init_indirect_dispatch_index)
4052
                    .unwrap_or_default(),
4053
                local_child_index,
4054
                global_child_index,
4055
                base_child_index,
4056
                particle_stride,
4057
                sort_key_offset,
4058
                sort_key2_offset,
4059
                ..default()
4060
            };
4061
            if let Some(indexed) = &cached_mesh.indexed {
×
4062
                gpu_effect_metadata.vertex_or_index_count = indexed.range.len() as u32;
4063
                gpu_effect_metadata.first_index_or_vertex_offset = indexed.range.start;
4064
                gpu_effect_metadata.vertex_offset_or_base_instance = cached_mesh.range.start as i32;
4065
            } else {
4066
                gpu_effect_metadata.vertex_or_index_count = cached_mesh.range.len() as u32;
×
4067
                gpu_effect_metadata.first_index_or_vertex_offset = cached_mesh.range.start;
×
4068
                gpu_effect_metadata.vertex_offset_or_base_instance = 0;
×
4069
            };
4070
            assert!(dispatch_buffer_indices
×
4071
                .effect_metadata_buffer_table_id
×
4072
                .is_valid());
×
4073
            effects_meta.effect_metadata_buffer.update(
×
4074
                dispatch_buffer_indices.effect_metadata_buffer_table_id,
×
4075
                gpu_effect_metadata,
×
4076
            );
4077

4078
            warn!(
×
4079
                "Updated metadata entry {} for effect {:?}, this will reset it.",
×
4080
                dispatch_buffer_indices.effect_metadata_buffer_table_id.0, main_entity
4081
            );
4082
        }
4083

4084
        prepared_effect_count += 1;
×
4085
    }
4086
    trace!("Prepared {prepared_effect_count}/{extracted_effect_count} extracted effect(s)");
×
4087

4088
    // Once all EffectMetadata values are written, schedule a GPU upload
4089
    if effects_meta
4090
        .effect_metadata_buffer
4091
        .allocate_gpu(render_device, render_queue)
4092
    {
4093
        // All those bind groups use the buffer so need to be re-created
4094
        trace!("*** Effect metadata buffer re-allocated; clearing all bind groups using it.");
×
4095
        effects_meta.indirect_metadata_bind_group = None;
×
4096
        effect_bind_groups.init_metadata_bind_groups.clear();
×
4097
        effect_bind_groups.update_metadata_bind_groups.clear();
×
4098
    }
4099

4100
    // Write the entire spawner buffer for this frame, for all effects combined
4101
    assert_eq!(
4102
        prepared_effect_count,
4103
        effects_meta.spawner_buffer.len() as u32
4104
    );
4105
    if effects_meta
×
4106
        .spawner_buffer
×
4107
        .write_buffer(render_device, render_queue)
×
4108
    {
4109
        // All property bind groups use the spawner buffer, which was reallocate
4110
        property_bind_groups.clear(true);
×
4111
        effects_meta.indirect_spawner_bind_group = None;
×
4112
    }
4113

4114
    // Update simulation parameters
4115
    effects_meta.sim_params_uniforms.set(sim_params.into());
×
4116
    {
4117
        let gpu_sim_params = effects_meta.sim_params_uniforms.get_mut();
×
4118
        gpu_sim_params.num_effects = prepared_effect_count;
×
4119

4120
        trace!(
×
4121
            "Simulation parameters: time={} delta_time={} virtual_time={} \
×
4122
                virtual_delta_time={} real_time={} real_delta_time={} num_effects={}",
×
4123
            gpu_sim_params.time,
4124
            gpu_sim_params.delta_time,
4125
            gpu_sim_params.virtual_time,
4126
            gpu_sim_params.virtual_delta_time,
4127
            gpu_sim_params.real_time,
4128
            gpu_sim_params.real_delta_time,
4129
            gpu_sim_params.num_effects,
4130
        );
4131
    }
4132
    let prev_buffer_id = effects_meta.sim_params_uniforms.buffer().map(|b| b.id());
×
4133
    effects_meta
×
4134
        .sim_params_uniforms
×
4135
        .write_buffer(render_device, render_queue);
×
4136
    if prev_buffer_id != effects_meta.sim_params_uniforms.buffer().map(|b| b.id()) {
×
4137
        // Buffer changed, invalidate bind groups
4138
        effects_meta.indirect_sim_params_bind_group = None;
×
4139
    }
4140
}
4141

4142
pub(crate) fn batch_effects(
×
4143
    mut commands: Commands,
4144
    effects_meta: Res<EffectsMeta>,
4145
    mut sort_bind_groups: ResMut<SortBindGroups>,
4146
    mut q_cached_effects: Query<(
4147
        Entity,
4148
        &CachedMesh,
4149
        Option<&CachedEffectEvents>,
4150
        Option<&CachedChildInfo>,
4151
        Option<&CachedProperties>,
4152
        &mut DispatchBufferIndices,
4153
        &mut BatchInput,
4154
    )>,
4155
    mut sorted_effect_batches: ResMut<SortedEffectBatches>,
4156
    mut gpu_buffer_operations: ResMut<GpuBufferOperations>,
4157
) {
4158
    trace!("batch_effects");
×
4159

4160
    // Sort first by effect buffer index, then by slice range (see EffectSlice)
4161
    // inside that buffer. This is critical for batching to work, because
4162
    // batching effects is based on compatible items, which implies same GPU
4163
    // buffer and continuous slice ranges (the next slice start must be equal to
4164
    // the previous start end, without gap). EffectSlice already contains both
4165
    // information, and the proper ordering implementation.
4166
    // effect_entity_list.sort_by_key(|a| a.effect_slice.clone());
4167

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

4172
    let mut sort_queue = GpuBufferOperationQueue::new();
×
4173

4174
    // Loop on all extracted effects in order, and try to batch them together to
4175
    // reduce draw calls. -- currently does nothing, batching was broken and never
4176
    // fixed.
4177
    // FIXME - This is in ECS order, if we re-add the sorting above we need a
4178
    // different order here!
4179
    trace!("Batching {} effects...", q_cached_effects.iter().len());
×
4180
    sorted_effect_batches.clear();
×
4181
    for (
4182
        entity,
×
4183
        cached_mesh,
×
4184
        cached_effect_events,
×
4185
        cached_child_info,
×
4186
        cached_properties,
×
4187
        dispatch_buffer_indices,
×
4188
        mut input,
×
4189
    ) in &mut q_cached_effects
×
4190
    {
4191
        // Detect if this cached effect was not updated this frame by a new extracted
4192
        // effect. This happens when e.g. the effect is invisible and not simulated, or
4193
        // some error prevented it from being extracted. We use the pipeline IDs vector
4194
        // as a marker, because each frame we move it out of the CachedGroup
4195
        // component during batching, so if empty this means a new one was not created
4196
        // this frame.
4197
        // if input.init_and_update_pipeline_ids.is_empty() {
4198
        //     trace!(
4199
        //         "Skipped cached effect on render entity {:?}: not extracted this
4200
        // frame.",         entity
4201
        //     );
4202
        //     continue;
4203
        // }
4204

4205
        let translation = input.position;
×
4206

4207
        // Spawn one EffectBatch per instance (no batching; TODO). This contains
4208
        // most of the data needed to drive rendering. However this doesn't drive
4209
        // rendering; this is just storage.
4210
        let mut effect_batch = EffectBatch::from_input(
4211
            cached_mesh,
×
4212
            cached_effect_events,
×
4213
            cached_child_info,
×
4214
            &mut input,
×
4215
            *dispatch_buffer_indices.as_ref(),
×
4216
            cached_properties.map(|cp| PropertyBindGroupKey {
×
4217
                buffer_index: cp.buffer_index,
×
4218
                binding_size: cp.binding_size,
×
4219
            }),
4220
            cached_properties.map(|cp| cp.offset),
×
4221
        );
4222

4223
        // If the batch has ribbons, we need to sort the particles by RIBBON_ID and AGE
4224
        // for ribbon meshing, in order to avoid gaps when some particles in the middle
4225
        // of the ribbon die (since we can't guarantee a linear lifetime through the
4226
        // ribbon).
4227
        if input.layout_flags.contains(LayoutFlags::RIBBONS) {
×
4228
            // This buffer is allocated in prepare_effects(), so should always be available
4229
            let Some(effect_metadata_buffer) = effects_meta.effect_metadata_buffer.buffer() else {
×
4230
                error!("Failed to find effect metadata buffer. This is a bug.");
×
4231
                continue;
×
4232
            };
4233

4234
            // Allocate a GpuDispatchIndirect entry
4235
            let sort_fill_indirect_dispatch_index = sort_bind_groups.allocate_indirect_dispatch();
4236
            effect_batch.sort_fill_indirect_dispatch_index =
4237
                Some(sort_fill_indirect_dispatch_index);
4238

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

4303
        let effect_batch_index = sorted_effect_batches.push(effect_batch);
×
4304
        trace!(
×
4305
            "Spawned effect batch #{:?} from cached instance on entity {:?}.",
×
4306
            effect_batch_index,
4307
            entity,
4308
        );
4309

4310
        // Spawn an EffectDrawBatch, to actually drive rendering.
4311
        commands
4312
            .spawn(EffectDrawBatch {
4313
                effect_batch_index,
4314
                translation,
4315
            })
4316
            .insert(TemporaryRenderEntity);
4317
    }
4318

4319
    debug_assert!(sorted_effect_batches.dispatch_queue_index.is_none());
×
4320
    if !sort_queue.operation_queue.is_empty() {
×
4321
        sorted_effect_batches.dispatch_queue_index = Some(gpu_buffer_operations.submit(sort_queue));
×
4322
    }
4323

4324
    sorted_effect_batches.sort();
×
4325
}
4326

4327
/// Per-buffer bind groups for a GPU effect buffer.
4328
///
4329
/// This contains all bind groups specific to a single [`EffectBuffer`].
4330
///
4331
/// [`EffectBuffer`]: crate::render::effect_cache::EffectBuffer
4332
pub(crate) struct BufferBindGroups {
4333
    /// Bind group for the render shader.
4334
    ///
4335
    /// ```wgsl
4336
    /// @binding(0) var<storage, read> particle_buffer : ParticleBuffer;
4337
    /// @binding(1) var<storage, read> indirect_buffer : IndirectBuffer;
4338
    /// @binding(2) var<storage, read> spawner : Spawner;
4339
    /// ```
4340
    render: BindGroup,
4341
    // /// Bind group for filling the indirect dispatch arguments of any child init
4342
    // /// pass.
4343
    // ///
4344
    // /// This bind group is optional; it's only created if the current effect has
4345
    // /// a GPU spawn event buffer, irrelevant of whether it has child effects
4346
    // /// (although normally the event buffer is not created if there's no
4347
    // /// children).
4348
    // ///
4349
    // /// The source buffer is always the current effect's event buffer. The
4350
    // /// destination buffer is the global shared buffer for indirect fill args
4351
    // /// operations owned by the [`EffectCache`]. The uniform buffer of operation
4352
    // /// args contains the data to index the relevant part of the global shared
4353
    // /// buffer for this effect buffer; it may contain multiple entries in case
4354
    // /// multiple effects are batched inside the current effect buffer.
4355
    // ///
4356
    // /// ```wgsl
4357
    // /// @group(0) @binding(0) var<uniform> args : BufferOperationArgs;
4358
    // /// @group(0) @binding(1) var<storage, read> src_buffer : array<u32>;
4359
    // /// @group(0) @binding(2) var<storage, read_write> dst_buffer : array<u32>;
4360
    // /// ```
4361
    // init_fill_dispatch: Option<BindGroup>,
4362
}
4363

4364
/// Combination of a texture layout and the bound textures.
4365
#[derive(Debug, Default, Clone, PartialEq, Eq, Hash)]
4366
struct Material {
4367
    layout: TextureLayout,
4368
    textures: Vec<AssetId<Image>>,
4369
}
4370

4371
impl Material {
4372
    /// Get the bind group entries to create a bind group.
4373
    pub fn make_entries<'a>(
×
4374
        &self,
4375
        gpu_images: &'a RenderAssets<GpuImage>,
4376
    ) -> Result<Vec<BindGroupEntry<'a>>, ()> {
4377
        if self.textures.is_empty() {
×
4378
            return Ok(vec![]);
×
4379
        }
4380

4381
        let entries: Vec<BindGroupEntry<'a>> = self
×
4382
            .textures
×
4383
            .iter()
4384
            .enumerate()
4385
            .flat_map(|(index, id)| {
×
4386
                let base_binding = index as u32 * 2;
×
4387
                if let Some(gpu_image) = gpu_images.get(*id) {
×
4388
                    vec![
×
4389
                        BindGroupEntry {
×
4390
                            binding: base_binding,
×
4391
                            resource: BindingResource::TextureView(&gpu_image.texture_view),
×
4392
                        },
4393
                        BindGroupEntry {
×
4394
                            binding: base_binding + 1,
×
4395
                            resource: BindingResource::Sampler(&gpu_image.sampler),
×
4396
                        },
4397
                    ]
4398
                } else {
4399
                    vec![]
×
4400
                }
4401
            })
4402
            .collect();
4403
        if entries.len() == self.textures.len() * 2 {
×
4404
            return Ok(entries);
×
4405
        }
4406
        Err(())
×
4407
    }
4408
}
4409

4410
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
4411
struct BindingKey {
4412
    pub buffer_id: BufferId,
4413
    pub offset: u32,
4414
    pub size: NonZeroU32,
4415
}
4416

4417
impl<'a> From<BufferSlice<'a>> for BindingKey {
4418
    fn from(value: BufferSlice<'a>) -> Self {
×
4419
        Self {
4420
            buffer_id: value.buffer.id(),
×
4421
            offset: value.offset,
×
4422
            size: value.size,
×
4423
        }
4424
    }
4425
}
4426

4427
impl<'a> From<&BufferSlice<'a>> for BindingKey {
4428
    fn from(value: &BufferSlice<'a>) -> Self {
×
4429
        Self {
4430
            buffer_id: value.buffer.id(),
×
4431
            offset: value.offset,
×
4432
            size: value.size,
×
4433
        }
4434
    }
4435
}
4436

4437
impl From<&BufferBindingSource> for BindingKey {
4438
    fn from(value: &BufferBindingSource) -> Self {
×
4439
        Self {
4440
            buffer_id: value.buffer.id(),
×
4441
            offset: value.offset,
×
4442
            size: value.size,
×
4443
        }
4444
    }
4445
}
4446

4447
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
4448
struct ConsumeEventKey {
4449
    child_infos_buffer_id: BufferId,
4450
    events: BindingKey,
4451
}
4452

4453
impl From<&ConsumeEventBuffers<'_>> for ConsumeEventKey {
4454
    fn from(value: &ConsumeEventBuffers) -> Self {
×
4455
        Self {
4456
            child_infos_buffer_id: value.child_infos_buffer.id(),
×
4457
            events: value.events.into(),
×
4458
        }
4459
    }
4460
}
4461

4462
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
4463
struct InitMetadataBindGroupKey {
4464
    pub buffer_index: u32,
4465
    pub effect_metadata_buffer: BufferId,
4466
    pub effect_metadata_offset: u32,
4467
    pub consume_event_key: Option<ConsumeEventKey>,
4468
}
4469

4470
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
4471
struct UpdateMetadataBindGroupKey {
4472
    pub buffer_index: u32,
4473
    pub effect_metadata_buffer: BufferId,
4474
    pub effect_metadata_offset: u32,
4475
    pub child_info_buffer_id: Option<BufferId>,
4476
    pub event_buffers_keys: Vec<BindingKey>,
4477
}
4478

4479
struct CachedBindGroup<K: Eq> {
4480
    /// Key the bind group was created from. Each time the key changes, the bind
4481
    /// group should be re-created.
4482
    key: K,
4483
    /// Bind group created from the key.
4484
    bind_group: BindGroup,
4485
}
4486

4487
#[derive(Debug, Clone, Copy)]
4488
struct BufferSlice<'a> {
4489
    pub buffer: &'a Buffer,
4490
    pub offset: u32,
4491
    pub size: NonZeroU32,
4492
}
4493

4494
impl<'a> From<BufferSlice<'a>> for BufferBinding<'a> {
4495
    fn from(value: BufferSlice<'a>) -> Self {
×
4496
        Self {
4497
            buffer: value.buffer,
×
4498
            offset: value.offset.into(),
×
4499
            size: Some(value.size.into()),
×
4500
        }
4501
    }
4502
}
4503

4504
impl<'a> From<&BufferSlice<'a>> for BufferBinding<'a> {
4505
    fn from(value: &BufferSlice<'a>) -> Self {
×
4506
        Self {
4507
            buffer: value.buffer,
×
4508
            offset: value.offset.into(),
×
4509
            size: Some(value.size.into()),
×
4510
        }
4511
    }
4512
}
4513

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

4524
/// Optional input to [`EffectBindGroups::get_or_create_init_metadata()`] when
4525
/// the init pass consumes GPU events as a mechanism to spawn particles.
4526
struct ConsumeEventBuffers<'a> {
4527
    /// Entire buffer containing the [`GpuChildInfo`] entries for all effects.
4528
    /// This is dynamically indexed inside the shader.
4529
    child_infos_buffer: &'a Buffer,
4530
    /// Slice of the [`EventBuffer`] where the GPU spawn events are stored.
4531
    events: BufferSlice<'a>,
4532
}
4533

4534
#[derive(Default, Resource)]
4535
pub struct EffectBindGroups {
4536
    /// Map from buffer index to the bind groups shared among all effects that
4537
    /// use that buffer.
4538
    particle_buffers: HashMap<u32, BufferBindGroups>,
4539
    /// Map of bind groups for image assets used as particle textures.
4540
    images: HashMap<AssetId<Image>, BindGroup>,
4541
    /// Map from buffer index to its metadata bind group (group 3) for the init
4542
    /// pass.
4543
    // FIXME - doesn't work with batching; this should be the instance ID
4544
    init_metadata_bind_groups: HashMap<u32, CachedBindGroup<InitMetadataBindGroupKey>>,
4545
    /// Map from buffer index to its metadata bind group (group 3) for the
4546
    /// update pass.
4547
    // FIXME - doesn't work with batching; this should be the instance ID
4548
    update_metadata_bind_groups: HashMap<u32, CachedBindGroup<UpdateMetadataBindGroupKey>>,
4549
    /// Map from an effect material to its bind group.
4550
    material_bind_groups: HashMap<Material, BindGroup>,
4551
}
4552

4553
impl EffectBindGroups {
4554
    pub fn particle_render(&self, buffer_index: u32) -> Option<&BindGroup> {
×
4555
        self.particle_buffers
×
4556
            .get(&buffer_index)
×
4557
            .map(|bg| &bg.render)
×
4558
    }
4559

4560
    /// Retrieve the metadata@3 bind group for the init pass, creating it if
4561
    /// needed.
4562
    pub(self) fn get_or_create_init_metadata(
×
4563
        &mut self,
4564
        effect_batch: &EffectBatch,
4565
        gpu_limits: &GpuLimits,
4566
        render_device: &RenderDevice,
4567
        layout: &BindGroupLayout,
4568
        effect_metadata_buffer: &Buffer,
4569
        consume_event_buffers: Option<ConsumeEventBuffers>,
4570
    ) -> Result<&BindGroup, ()> {
4571
        let DispatchBufferIndices {
×
4572
            effect_metadata_buffer_table_id,
×
4573
            ..
×
4574
        } = &effect_batch.dispatch_buffer_indices;
×
4575

4576
        let effect_metadata_offset =
×
4577
            gpu_limits.effect_metadata_offset(effect_metadata_buffer_table_id.0) as u32;
×
4578
        let key = InitMetadataBindGroupKey {
4579
            buffer_index: effect_batch.buffer_index,
×
4580
            effect_metadata_buffer: effect_metadata_buffer.id(),
×
4581
            effect_metadata_offset,
4582
            consume_event_key: consume_event_buffers.as_ref().map(Into::into),
×
4583
        };
4584

4585
        let make_entry = || {
×
4586
            let mut entries = Vec::with_capacity(3);
×
4587
            entries.push(
×
4588
                // @group(3) @binding(0) var<storage, read_write> effect_metadata : EffectMetadata;
4589
                BindGroupEntry {
×
4590
                    binding: 0,
×
4591
                    resource: BindingResource::Buffer(BufferBinding {
×
4592
                        buffer: effect_metadata_buffer,
×
4593
                        offset: key.effect_metadata_offset as u64,
×
4594
                        size: Some(gpu_limits.effect_metadata_size()),
×
4595
                    }),
4596
                },
4597
            );
4598
            if let Some(consume_event_buffers) = consume_event_buffers.as_ref() {
×
4599
                entries.push(
4600
                    // @group(3) @binding(1) var<storage, read> child_info_buffer :
4601
                    // ChildInfoBuffer;
4602
                    BindGroupEntry {
4603
                        binding: 1,
4604
                        resource: BindingResource::Buffer(BufferBinding {
4605
                            buffer: consume_event_buffers.child_infos_buffer,
4606
                            offset: 0,
4607
                            size: None,
4608
                        }),
4609
                    },
4610
                );
4611
                entries.push(
4612
                    // @group(3) @binding(2) var<storage, read> event_buffer : EventBuffer;
4613
                    BindGroupEntry {
4614
                        binding: 2,
4615
                        resource: BindingResource::Buffer(consume_event_buffers.events.into()),
4616
                    },
4617
                );
4618
            }
4619

4620
            let bind_group = render_device.create_bind_group(
×
4621
                "hanabi:bind_group:init:metadata@3",
4622
                layout,
×
4623
                &entries[..],
×
4624
            );
4625

4626
            trace!(
×
4627
                    "Created new metadata@3 bind group for init pass and buffer index {}: effect_metadata=#{}",
×
4628
                    effect_batch.buffer_index,
4629
                    effect_metadata_buffer_table_id.0,
4630
                );
4631

4632
            bind_group
×
4633
        };
4634

4635
        Ok(&self
×
4636
            .init_metadata_bind_groups
×
4637
            .entry(effect_batch.buffer_index)
×
4638
            .and_modify(|cbg| {
×
4639
                if cbg.key != key {
×
4640
                    trace!(
×
4641
                        "Bind group key changed for init metadata@3, re-creating bind group... old={:?} new={:?}",
×
4642
                        cbg.key,
4643
                        key
4644
                    );
4645
                    cbg.key = key;
×
4646
                    cbg.bind_group = make_entry();
×
4647
                }
4648
            })
4649
            .or_insert_with(|| {
×
4650
                trace!("Inserting new bind group for init metadata@3 with key={:?}", key);
×
4651
                CachedBindGroup {
×
4652
                    key,
×
4653
                    bind_group: make_entry(),
×
4654
                }
4655
            })
4656
            .bind_group)
×
4657
    }
4658

4659
    /// Retrieve the metadata@3 bind group for the update pass, creating it if
4660
    /// needed.
4661
    pub(self) fn get_or_create_update_metadata(
×
4662
        &mut self,
4663
        effect_batch: &EffectBatch,
4664
        gpu_limits: &GpuLimits,
4665
        render_device: &RenderDevice,
4666
        layout: &BindGroupLayout,
4667
        effect_metadata_buffer: &Buffer,
4668
        child_info_buffer: Option<&Buffer>,
4669
        event_buffers: &[(Entity, BufferBindingSource)],
4670
    ) -> Result<&BindGroup, ()> {
4671
        let DispatchBufferIndices {
×
4672
            effect_metadata_buffer_table_id,
×
4673
            ..
×
4674
        } = &effect_batch.dispatch_buffer_indices;
×
4675

4676
        // Check arguments consistency
4677
        assert_eq!(effect_batch.child_event_buffers.len(), event_buffers.len());
×
4678
        let emits_gpu_spawn_events = !event_buffers.is_empty();
×
4679
        let child_info_buffer_id = if emits_gpu_spawn_events {
×
4680
            child_info_buffer.as_ref().map(|buffer| buffer.id())
×
4681
        } else {
4682
            // Note: child_info_buffer can be Some() if allocated, but we only consider it
4683
            // if relevant, that is if the effect emits GPU spawn events.
4684
            None
×
4685
        };
4686
        assert_eq!(emits_gpu_spawn_events, child_info_buffer_id.is_some());
×
4687

4688
        let event_buffers_keys = event_buffers
×
4689
            .iter()
4690
            .map(|(_, buffer_binding_source)| buffer_binding_source.into())
×
4691
            .collect::<Vec<_>>();
4692

4693
        let key = UpdateMetadataBindGroupKey {
4694
            buffer_index: effect_batch.buffer_index,
×
4695
            effect_metadata_buffer: effect_metadata_buffer.id(),
×
4696
            effect_metadata_offset: gpu_limits
×
4697
                .effect_metadata_offset(effect_metadata_buffer_table_id.0)
4698
                as u32,
4699
            child_info_buffer_id,
4700
            event_buffers_keys,
4701
        };
4702

4703
        let make_entry = || {
×
4704
            let mut entries = Vec::with_capacity(2 + event_buffers.len());
×
4705
            // @group(3) @binding(0) var<storage, read_write> effect_metadata :
4706
            // EffectMetadata;
4707
            entries.push(BindGroupEntry {
×
4708
                binding: 0,
×
4709
                resource: BindingResource::Buffer(BufferBinding {
×
4710
                    buffer: effect_metadata_buffer,
×
4711
                    offset: key.effect_metadata_offset as u64,
×
4712
                    size: Some(gpu_limits.effect_metadata_aligned_size.into()),
×
4713
                }),
4714
            });
4715
            if emits_gpu_spawn_events {
×
4716
                let child_info_buffer = child_info_buffer.unwrap();
×
4717

4718
                // @group(3) @binding(1) var<storage, read_write> child_info_buffer :
4719
                // ChildInfoBuffer;
4720
                entries.push(BindGroupEntry {
×
4721
                    binding: 1,
×
4722
                    resource: BindingResource::Buffer(BufferBinding {
×
4723
                        buffer: child_info_buffer,
×
4724
                        offset: 0,
×
4725
                        size: None,
×
4726
                    }),
4727
                });
4728

4729
                for (index, (_, buffer_binding_source)) in event_buffers.iter().enumerate() {
×
4730
                    // @group(3) @binding(2+N) var<storage, read_write> event_buffer_N :
4731
                    // EventBuffer;
4732
                    // FIXME - BufferBindingSource originally was for Events, counting in u32, but
4733
                    // then moved to counting in bytes, so now need some conversion. Need to review
4734
                    // all of this...
4735
                    let mut buffer_binding: BufferBinding = buffer_binding_source.into();
4736
                    buffer_binding.offset *= 4;
4737
                    buffer_binding.size = buffer_binding
4738
                        .size
4739
                        .map(|sz| NonZeroU64::new(sz.get() * 4).unwrap());
×
4740
                    entries.push(BindGroupEntry {
4741
                        binding: 2 + index as u32,
4742
                        resource: BindingResource::Buffer(buffer_binding),
4743
                    });
4744
                }
4745
            }
4746

4747
            let bind_group = render_device.create_bind_group(
×
4748
                "hanabi:bind_group:update:metadata@3",
4749
                layout,
×
4750
                &entries[..],
×
4751
            );
4752

4753
            trace!(
×
4754
                "Created new metadata@3 bind group for update pass and buffer index {}: effect_metadata={}",
×
4755
                effect_batch.buffer_index,
4756
                effect_metadata_buffer_table_id.0,
4757
            );
4758

4759
            bind_group
×
4760
        };
4761

4762
        Ok(&self
×
4763
            .update_metadata_bind_groups
×
4764
            .entry(effect_batch.buffer_index)
×
4765
            .and_modify(|cbg| {
×
4766
                if cbg.key != key {
×
4767
                    trace!(
×
4768
                        "Bind group key changed for update metadata@3, re-creating bind group... old={:?} new={:?}",
×
4769
                        cbg.key,
4770
                        key
4771
                    );
4772
                    cbg.key = key.clone();
×
4773
                    cbg.bind_group = make_entry();
×
4774
                }
4775
            })
4776
            .or_insert_with(|| {
×
4777
                trace!(
×
4778
                    "Inserting new bind group for update metadata@3 with key={:?}",
×
4779
                    key
4780
                );
4781
                CachedBindGroup {
×
4782
                    key: key.clone(),
×
4783
                    bind_group: make_entry(),
×
4784
                }
4785
            })
4786
            .bind_group)
×
4787
    }
4788
}
4789

4790
#[derive(SystemParam)]
4791
pub struct QueueEffectsReadOnlyParams<'w, 's> {
4792
    #[cfg(feature = "2d")]
4793
    draw_functions_2d: Res<'w, DrawFunctions<Transparent2d>>,
4794
    #[cfg(feature = "3d")]
4795
    draw_functions_3d: Res<'w, DrawFunctions<Transparent3d>>,
4796
    #[cfg(feature = "3d")]
4797
    draw_functions_alpha_mask: Res<'w, DrawFunctions<AlphaMask3d>>,
4798
    #[cfg(feature = "3d")]
4799
    draw_functions_opaque: Res<'w, DrawFunctions<Opaque3d>>,
4800
    #[system_param(ignore)]
4801
    marker: PhantomData<&'s usize>,
4802
}
4803

4804
fn emit_sorted_draw<T, F>(
×
4805
    views: &Query<(Entity, &RenderVisibleEntities, &ExtractedView, &Msaa)>,
4806
    render_phases: &mut ResMut<ViewSortedRenderPhases<T>>,
4807
    view_entities: &mut FixedBitSet,
4808
    sorted_effect_batches: &SortedEffectBatches,
4809
    effect_draw_batches: &Query<(Entity, &mut EffectDrawBatch)>,
4810
    render_pipeline: &mut ParticlesRenderPipeline,
4811
    mut specialized_render_pipelines: Mut<SpecializedRenderPipelines<ParticlesRenderPipeline>>,
4812
    render_meshes: &RenderAssets<RenderMesh>,
4813
    pipeline_cache: &PipelineCache,
4814
    make_phase_item: F,
4815
    #[cfg(all(feature = "2d", feature = "3d"))] pipeline_mode: PipelineMode,
4816
) where
4817
    T: SortedPhaseItem,
4818
    F: Fn(CachedRenderPipelineId, (Entity, MainEntity), &EffectDrawBatch, &ExtractedView) -> T,
4819
{
4820
    trace!("emit_sorted_draw() {} views", views.iter().len());
×
4821

4822
    for (view_entity, visible_entities, view, msaa) in views.iter() {
×
4823
        trace!(
×
4824
            "Process new sorted view with {} visible particle effect entities",
×
4825
            visible_entities.len::<WithCompiledParticleEffect>()
×
4826
        );
4827

4828
        let Some(render_phase) = render_phases.get_mut(&view_entity) else {
×
4829
            continue;
×
4830
        };
4831

4832
        {
4833
            #[cfg(feature = "trace")]
4834
            let _span = bevy::utils::tracing::info_span!("collect_view_entities").entered();
×
4835

4836
            view_entities.clear();
×
4837
            view_entities.extend(
×
4838
                visible_entities
×
4839
                    .iter::<WithCompiledParticleEffect>()
×
4840
                    .map(|e| e.1.index() as usize),
×
4841
            );
4842
        }
4843

4844
        // For each view, loop over all the effect batches to determine if the effect
4845
        // needs to be rendered for that view, and enqueue a view-dependent
4846
        // batch if so.
4847
        for (draw_entity, draw_batch) in effect_draw_batches.iter() {
×
4848
            #[cfg(feature = "trace")]
4849
            let _span_draw = bevy::utils::tracing::info_span!("draw_batch").entered();
×
4850

4851
            trace!(
×
4852
                "Process draw batch: draw_entity={:?} effect_batch_index={:?}",
×
4853
                draw_entity,
×
4854
                draw_batch.effect_batch_index,
×
4855
            );
4856

4857
            // Get the EffectBatches this EffectDrawBatch is part of.
4858
            let Some(effect_batch) = sorted_effect_batches.get(draw_batch.effect_batch_index)
×
4859
            else {
×
4860
                continue;
×
4861
            };
4862

4863
            trace!(
×
4864
                "-> EffectBach: buffer_index={} spawner_base={} layout_flags={:?}",
×
4865
                effect_batch.buffer_index,
×
4866
                effect_batch.spawner_base,
×
4867
                effect_batch.layout_flags,
×
4868
            );
4869

4870
            // AlphaMask is a binned draw, so no sorted draw can possibly use it
4871
            if effect_batch
×
4872
                .layout_flags
×
4873
                .intersects(LayoutFlags::USE_ALPHA_MASK | LayoutFlags::OPAQUE)
×
4874
            {
4875
                trace!("Non-transparent batch. Skipped.");
×
4876
                continue;
×
4877
            }
4878

4879
            // Check if batch contains any entity visible in the current view. Otherwise we
4880
            // can skip the entire batch. Note: This is O(n^2) but (unlike
4881
            // the Sprite renderer this is inspired from) we don't expect more than
4882
            // a handful of particle effect instances, so would rather not pay the memory
4883
            // cost of a FixedBitSet for the sake of an arguable speed-up.
4884
            // TODO - Profile to confirm.
4885
            #[cfg(feature = "trace")]
4886
            let _span_check_vis = bevy::utils::tracing::info_span!("check_visibility").entered();
×
4887
            let has_visible_entity = effect_batch
×
4888
                .entities
×
4889
                .iter()
4890
                .any(|index| view_entities.contains(*index as usize));
×
4891
            if !has_visible_entity {
×
4892
                trace!("No visible entity for view, not emitting any draw call.");
×
4893
                continue;
×
4894
            }
4895
            #[cfg(feature = "trace")]
4896
            _span_check_vis.exit();
×
4897

4898
            // Create and cache the bind group layout for this texture layout
4899
            render_pipeline.cache_material(&effect_batch.texture_layout);
×
4900

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

4904
            let local_space_simulation = effect_batch
×
4905
                .layout_flags
×
4906
                .contains(LayoutFlags::LOCAL_SPACE_SIMULATION);
×
4907
            let alpha_mask = ParticleRenderAlphaMaskPipelineKey::from(effect_batch.layout_flags);
×
4908
            let flipbook = effect_batch.layout_flags.contains(LayoutFlags::FLIPBOOK);
×
4909
            let needs_uv = effect_batch.layout_flags.contains(LayoutFlags::NEEDS_UV);
×
4910
            let needs_normal = effect_batch
×
4911
                .layout_flags
×
4912
                .contains(LayoutFlags::NEEDS_NORMAL);
×
4913
            let ribbons = effect_batch.layout_flags.contains(LayoutFlags::RIBBONS);
×
4914
            let image_count = effect_batch.texture_layout.layout.len() as u8;
×
4915

4916
            // FIXME - Maybe it's better to copy the mesh layout into the batch, instead of
4917
            // re-querying here...?
4918
            let Some(render_mesh) = render_meshes.get(effect_batch.mesh) else {
×
4919
                trace!("Batch has no render mesh, skipped.");
×
4920
                continue;
×
4921
            };
4922
            let mesh_layout = render_mesh.layout.clone();
×
4923

4924
            // Specialize the render pipeline based on the effect batch
4925
            trace!(
×
4926
                "Specializing render pipeline: render_shader={:?} image_count={} alpha_mask={:?} flipbook={:?} hdr={}",
×
4927
                effect_batch.render_shader,
×
4928
                image_count,
×
4929
                alpha_mask,
×
4930
                flipbook,
×
4931
                view.hdr
×
4932
            );
4933

4934
            // Add a draw pass for the effect batch
4935
            trace!("Emitting individual draw for batch");
×
4936

4937
            let alpha_mode = effect_batch.alpha_mode;
×
4938

4939
            #[cfg(feature = "trace")]
4940
            let _span_specialize = bevy::utils::tracing::info_span!("specialize").entered();
×
4941
            let render_pipeline_id = specialized_render_pipelines.specialize(
×
4942
                pipeline_cache,
×
4943
                render_pipeline,
×
4944
                ParticleRenderPipelineKey {
×
4945
                    shader: effect_batch.render_shader.clone(),
×
4946
                    mesh_layout: Some(mesh_layout),
×
4947
                    particle_layout: effect_batch.particle_layout.clone(),
×
4948
                    texture_layout: effect_batch.texture_layout.clone(),
×
4949
                    local_space_simulation,
×
4950
                    alpha_mask,
×
4951
                    alpha_mode,
×
4952
                    flipbook,
×
4953
                    needs_uv,
×
4954
                    needs_normal,
×
4955
                    ribbons,
×
4956
                    #[cfg(all(feature = "2d", feature = "3d"))]
×
4957
                    pipeline_mode,
×
4958
                    msaa_samples: msaa.samples(),
×
4959
                    hdr: view.hdr,
×
4960
                },
4961
            );
4962
            #[cfg(feature = "trace")]
4963
            _span_specialize.exit();
×
4964

4965
            trace!("+ Render pipeline specialized: id={:?}", render_pipeline_id,);
×
4966
            trace!(
×
4967
                "+ Add Transparent for batch on draw_entity {:?}: buffer_index={} \
×
4968
                spawner_base={} handle={:?}",
×
4969
                draw_entity,
×
4970
                effect_batch.buffer_index,
×
4971
                effect_batch.spawner_base,
×
4972
                effect_batch.handle
×
4973
            );
4974
            render_phase.add(make_phase_item(
×
4975
                render_pipeline_id,
×
4976
                (draw_entity, MainEntity::from(Entity::PLACEHOLDER)),
×
4977
                draw_batch,
×
4978
                view,
×
4979
            ));
4980
        }
4981
    }
4982
}
4983

4984
#[cfg(feature = "3d")]
4985
fn emit_binned_draw<T, F>(
×
4986
    views: &Query<(Entity, &RenderVisibleEntities, &ExtractedView, &Msaa)>,
4987
    render_phases: &mut ResMut<ViewBinnedRenderPhases<T>>,
4988
    view_entities: &mut FixedBitSet,
4989
    sorted_effect_batches: &SortedEffectBatches,
4990
    effect_draw_batches: &Query<(Entity, &mut EffectDrawBatch)>,
4991
    render_pipeline: &mut ParticlesRenderPipeline,
4992
    mut specialized_render_pipelines: Mut<SpecializedRenderPipelines<ParticlesRenderPipeline>>,
4993
    pipeline_cache: &PipelineCache,
4994
    render_meshes: &RenderAssets<RenderMesh>,
4995
    make_bin_key: F,
4996
    #[cfg(all(feature = "2d", feature = "3d"))] pipeline_mode: PipelineMode,
4997
    alpha_mask: ParticleRenderAlphaMaskPipelineKey,
4998
) where
4999
    T: BinnedPhaseItem,
5000
    F: Fn(CachedRenderPipelineId, &EffectDrawBatch, &ExtractedView) -> T::BinKey,
5001
{
5002
    use bevy::render::render_phase::BinnedRenderPhaseType;
5003

5004
    trace!("emit_binned_draw() {} views", views.iter().len());
×
5005

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

5009
        let Some(render_phase) = render_phases.get_mut(&view_entity) else {
×
5010
            continue;
×
5011
        };
5012

5013
        {
5014
            #[cfg(feature = "trace")]
5015
            let _span = bevy::utils::tracing::info_span!("collect_view_entities").entered();
×
5016

5017
            view_entities.clear();
×
5018
            view_entities.extend(
×
5019
                visible_entities
×
5020
                    .iter::<WithCompiledParticleEffect>()
×
5021
                    .map(|e| e.1.index() as usize),
×
5022
            );
5023
        }
5024

5025
        // For each view, loop over all the effect batches to determine if the effect
5026
        // needs to be rendered for that view, and enqueue a view-dependent
5027
        // batch if so.
5028
        for (draw_entity, draw_batch) in effect_draw_batches.iter() {
×
5029
            #[cfg(feature = "trace")]
5030
            let _span_draw = bevy::utils::tracing::info_span!("draw_batch").entered();
×
5031

5032
            trace!(
×
5033
                "Process draw batch: draw_entity={:?} effect_batch_index={:?}",
×
5034
                draw_entity,
×
5035
                draw_batch.effect_batch_index,
×
5036
            );
5037

5038
            // Get the EffectBatches this EffectDrawBatch is part of.
5039
            let Some(effect_batch) = sorted_effect_batches.get(draw_batch.effect_batch_index)
×
5040
            else {
×
5041
                continue;
×
5042
            };
5043

5044
            trace!(
×
5045
                "-> EffectBaches: buffer_index={} spawner_base={} layout_flags={:?}",
×
5046
                effect_batch.buffer_index,
×
5047
                effect_batch.spawner_base,
×
5048
                effect_batch.layout_flags,
×
5049
            );
5050

5051
            if ParticleRenderAlphaMaskPipelineKey::from(effect_batch.layout_flags) != alpha_mask {
×
5052
                trace!(
×
5053
                    "Mismatching alpha mask pipeline key (batches={:?}, expected={:?}). Skipped.",
×
5054
                    effect_batch.layout_flags,
×
5055
                    alpha_mask
×
5056
                );
5057
                continue;
×
5058
            }
5059

5060
            // Check if batch contains any entity visible in the current view. Otherwise we
5061
            // can skip the entire batch. Note: This is O(n^2) but (unlike
5062
            // the Sprite renderer this is inspired from) we don't expect more than
5063
            // a handful of particle effect instances, so would rather not pay the memory
5064
            // cost of a FixedBitSet for the sake of an arguable speed-up.
5065
            // TODO - Profile to confirm.
5066
            #[cfg(feature = "trace")]
5067
            let _span_check_vis = bevy::utils::tracing::info_span!("check_visibility").entered();
×
5068
            let has_visible_entity = effect_batch
×
5069
                .entities
×
5070
                .iter()
5071
                .any(|index| view_entities.contains(*index as usize));
×
5072
            if !has_visible_entity {
×
5073
                trace!("No visible entity for view, not emitting any draw call.");
×
5074
                continue;
×
5075
            }
5076
            #[cfg(feature = "trace")]
5077
            _span_check_vis.exit();
×
5078

5079
            // Create and cache the bind group layout for this texture layout
5080
            render_pipeline.cache_material(&effect_batch.texture_layout);
×
5081

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

5085
            let local_space_simulation = effect_batch
×
5086
                .layout_flags
×
5087
                .contains(LayoutFlags::LOCAL_SPACE_SIMULATION);
×
5088
            let alpha_mask = ParticleRenderAlphaMaskPipelineKey::from(effect_batch.layout_flags);
×
5089
            let flipbook = effect_batch.layout_flags.contains(LayoutFlags::FLIPBOOK);
×
5090
            let needs_uv = effect_batch.layout_flags.contains(LayoutFlags::NEEDS_UV);
×
5091
            let needs_normal = effect_batch
×
5092
                .layout_flags
×
5093
                .contains(LayoutFlags::NEEDS_NORMAL);
×
5094
            let ribbons = effect_batch.layout_flags.contains(LayoutFlags::RIBBONS);
×
5095
            let image_count = effect_batch.texture_layout.layout.len() as u8;
×
5096
            let render_mesh = render_meshes.get(effect_batch.mesh);
×
5097

5098
            // Specialize the render pipeline based on the effect batch
5099
            trace!(
×
5100
                "Specializing render pipeline: render_shaders={:?} image_count={} alpha_mask={:?} flipbook={:?} hdr={}",
×
5101
                effect_batch.render_shader,
×
5102
                image_count,
×
5103
                alpha_mask,
×
5104
                flipbook,
×
5105
                view.hdr
×
5106
            );
5107

5108
            // Add a draw pass for the effect batch
5109
            trace!("Emitting individual draw for batch");
×
5110

5111
            let alpha_mode = effect_batch.alpha_mode;
×
5112

5113
            let Some(mesh_layout) = render_mesh.map(|gpu_mesh| gpu_mesh.layout.clone()) else {
×
5114
                trace!("Missing mesh vertex buffer layout. Skipped.");
×
5115
                continue;
×
5116
            };
5117

5118
            #[cfg(feature = "trace")]
5119
            let _span_specialize = bevy::utils::tracing::info_span!("specialize").entered();
×
5120
            let render_pipeline_id = specialized_render_pipelines.specialize(
×
5121
                pipeline_cache,
×
5122
                render_pipeline,
×
5123
                ParticleRenderPipelineKey {
×
5124
                    shader: effect_batch.render_shader.clone(),
×
5125
                    mesh_layout: Some(mesh_layout),
×
5126
                    particle_layout: effect_batch.particle_layout.clone(),
×
5127
                    texture_layout: effect_batch.texture_layout.clone(),
×
5128
                    local_space_simulation,
×
5129
                    alpha_mask,
×
5130
                    alpha_mode,
×
5131
                    flipbook,
×
5132
                    needs_uv,
×
5133
                    needs_normal,
×
5134
                    ribbons,
×
5135
                    #[cfg(all(feature = "2d", feature = "3d"))]
×
5136
                    pipeline_mode,
×
5137
                    msaa_samples: msaa.samples(),
×
5138
                    hdr: view.hdr,
×
5139
                },
5140
            );
5141
            #[cfg(feature = "trace")]
5142
            _span_specialize.exit();
×
5143

5144
            trace!("+ Render pipeline specialized: id={:?}", render_pipeline_id,);
×
5145
            trace!(
×
5146
                "+ Add Transparent for batch on draw_entity {:?}: buffer_index={} \
×
5147
                spawner_base={} handle={:?}",
×
5148
                draw_entity,
×
5149
                effect_batch.buffer_index,
×
5150
                effect_batch.spawner_base,
×
5151
                effect_batch.handle
×
5152
            );
5153
            render_phase.add(
×
5154
                make_bin_key(render_pipeline_id, draw_batch, view),
×
5155
                (draw_entity, MainEntity::from(Entity::PLACEHOLDER)),
×
5156
                BinnedRenderPhaseType::NonMesh,
×
5157
            );
5158
        }
5159
    }
5160
}
5161

5162
#[allow(clippy::too_many_arguments)]
5163
pub(crate) fn queue_effects(
×
5164
    views: Query<(Entity, &RenderVisibleEntities, &ExtractedView, &Msaa)>,
5165
    effects_meta: Res<EffectsMeta>,
5166
    mut render_pipeline: ResMut<ParticlesRenderPipeline>,
5167
    mut specialized_render_pipelines: ResMut<SpecializedRenderPipelines<ParticlesRenderPipeline>>,
5168
    pipeline_cache: Res<PipelineCache>,
5169
    mut effect_bind_groups: ResMut<EffectBindGroups>,
5170
    sorted_effect_batches: Res<SortedEffectBatches>,
5171
    effect_draw_batches: Query<(Entity, &mut EffectDrawBatch)>,
5172
    events: Res<EffectAssetEvents>,
5173
    render_meshes: Res<RenderAssets<RenderMesh>>,
5174
    read_params: QueueEffectsReadOnlyParams,
5175
    mut view_entities: Local<FixedBitSet>,
5176
    #[cfg(feature = "2d")] mut transparent_2d_render_phases: ResMut<
5177
        ViewSortedRenderPhases<Transparent2d>,
5178
    >,
5179
    #[cfg(feature = "3d")] mut transparent_3d_render_phases: ResMut<
5180
        ViewSortedRenderPhases<Transparent3d>,
5181
    >,
5182
    #[cfg(feature = "3d")] mut alpha_mask_3d_render_phases: ResMut<
5183
        ViewBinnedRenderPhases<AlphaMask3d>,
5184
    >,
5185
) {
5186
    #[cfg(feature = "trace")]
5187
    let _span = bevy::utils::tracing::info_span!("hanabi:queue_effects").entered();
×
5188

5189
    trace!("queue_effects");
×
5190

5191
    // If an image has changed, the GpuImage has (probably) changed
5192
    for event in &events.images {
×
5193
        match event {
5194
            AssetEvent::Added { .. } => None,
×
5195
            AssetEvent::LoadedWithDependencies { .. } => None,
×
5196
            AssetEvent::Unused { .. } => None,
×
5197
            AssetEvent::Modified { id } => {
×
5198
                trace!("Destroy bind group of modified image asset {:?}", id);
×
5199
                effect_bind_groups.images.remove(id)
×
5200
            }
5201
            AssetEvent::Removed { id } => {
×
5202
                trace!("Destroy bind group of removed image asset {:?}", id);
×
5203
                effect_bind_groups.images.remove(id)
×
5204
            }
5205
        };
5206
    }
5207

5208
    if effects_meta.spawner_buffer.buffer().is_none() || effects_meta.spawner_buffer.is_empty() {
×
5209
        // No spawners are active
5210
        return;
×
5211
    }
5212

5213
    // Loop over all 2D cameras/views that need to render effects
5214
    #[cfg(feature = "2d")]
5215
    {
5216
        #[cfg(feature = "trace")]
5217
        let _span_draw = bevy::utils::tracing::info_span!("draw_2d").entered();
5218

5219
        let draw_effects_function_2d = read_params
5220
            .draw_functions_2d
5221
            .read()
5222
            .get_id::<DrawEffects>()
5223
            .unwrap();
5224

5225
        // Effects with full alpha blending
5226
        if !views.is_empty() {
5227
            trace!("Emit effect draw calls for alpha blended 2D views...");
×
5228
            emit_sorted_draw(
5229
                &views,
5230
                &mut transparent_2d_render_phases,
5231
                &mut view_entities,
5232
                &sorted_effect_batches,
5233
                &effect_draw_batches,
5234
                &mut render_pipeline,
5235
                specialized_render_pipelines.reborrow(),
5236
                &render_meshes,
5237
                &pipeline_cache,
5238
                |id, entity, draw_batch, _view| Transparent2d {
×
5239
                    sort_key: FloatOrd(draw_batch.translation.z),
×
5240
                    entity,
×
5241
                    pipeline: id,
×
5242
                    draw_function: draw_effects_function_2d,
×
5243
                    batch_range: 0..1,
×
5244
                    extra_index: PhaseItemExtraIndex::NONE,
×
5245
                },
5246
                #[cfg(feature = "3d")]
5247
                PipelineMode::Camera2d,
5248
            );
5249
        }
5250
    }
5251

5252
    // Loop over all 3D cameras/views that need to render effects
5253
    #[cfg(feature = "3d")]
5254
    {
5255
        #[cfg(feature = "trace")]
5256
        let _span_draw = bevy::utils::tracing::info_span!("draw_3d").entered();
5257

5258
        // Effects with full alpha blending
5259
        if !views.is_empty() {
5260
            trace!("Emit effect draw calls for alpha blended 3D views...");
×
5261

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

5268
            emit_sorted_draw(
5269
                &views,
5270
                &mut transparent_3d_render_phases,
5271
                &mut view_entities,
5272
                &sorted_effect_batches,
5273
                &effect_draw_batches,
5274
                &mut render_pipeline,
5275
                specialized_render_pipelines.reborrow(),
5276
                &render_meshes,
5277
                &pipeline_cache,
5278
                |id, entity, batch, view| Transparent3d {
×
5279
                    draw_function: draw_effects_function_3d,
×
5280
                    pipeline: id,
×
5281
                    entity,
×
5282
                    distance: view
×
5283
                        .rangefinder3d()
×
5284
                        .distance_translation(&batch.translation),
×
5285
                    batch_range: 0..1,
×
5286
                    extra_index: PhaseItemExtraIndex::NONE,
×
5287
                },
5288
                #[cfg(feature = "2d")]
5289
                PipelineMode::Camera3d,
5290
            );
5291
        }
5292

5293
        // Effects with alpha mask
5294
        if !views.is_empty() {
5295
            #[cfg(feature = "trace")]
5296
            let _span_draw = bevy::utils::tracing::info_span!("draw_alphamask").entered();
×
5297

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

5300
            let draw_effects_function_alpha_mask = read_params
5301
                .draw_functions_alpha_mask
5302
                .read()
5303
                .get_id::<DrawEffects>()
5304
                .unwrap();
5305

5306
            emit_binned_draw(
5307
                &views,
5308
                &mut alpha_mask_3d_render_phases,
5309
                &mut view_entities,
5310
                &sorted_effect_batches,
5311
                &effect_draw_batches,
5312
                &mut render_pipeline,
5313
                specialized_render_pipelines.reborrow(),
5314
                &pipeline_cache,
5315
                &render_meshes,
5316
                |id, _batch, _view| OpaqueNoLightmap3dBinKey {
×
5317
                    pipeline: id,
×
5318
                    draw_function: draw_effects_function_alpha_mask,
×
5319
                    asset_id: AssetId::<Image>::default().untyped(),
×
5320
                    material_bind_group_id: None,
×
5321
                    // },
5322
                    // distance: view
5323
                    //     .rangefinder3d()
5324
                    //     .distance_translation(&batch.translation_3d),
5325
                    // batch_range: 0..1,
5326
                    // extra_index: PhaseItemExtraIndex::NONE,
5327
                },
5328
                #[cfg(feature = "2d")]
5329
                PipelineMode::Camera3d,
5330
                ParticleRenderAlphaMaskPipelineKey::AlphaMask,
5331
            );
5332
        }
5333

5334
        // Opaque particles
5335
        if !views.is_empty() {
5336
            #[cfg(feature = "trace")]
5337
            let _span_draw = bevy::utils::tracing::info_span!("draw_opaque").entered();
×
5338

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

5341
            let draw_effects_function_opaque = read_params
5342
                .draw_functions_opaque
5343
                .read()
5344
                .get_id::<DrawEffects>()
5345
                .unwrap();
5346

5347
            emit_binned_draw(
5348
                &views,
5349
                &mut alpha_mask_3d_render_phases,
5350
                &mut view_entities,
5351
                &sorted_effect_batches,
5352
                &effect_draw_batches,
5353
                &mut render_pipeline,
5354
                specialized_render_pipelines.reborrow(),
5355
                &pipeline_cache,
5356
                &render_meshes,
5357
                |id, _batch, _view| OpaqueNoLightmap3dBinKey {
×
5358
                    pipeline: id,
×
5359
                    draw_function: draw_effects_function_opaque,
×
5360
                    asset_id: AssetId::<Image>::default().untyped(),
×
5361
                    material_bind_group_id: None,
×
5362
                    // },
5363
                    // distance: view
5364
                    //     .rangefinder3d()
5365
                    //     .distance_translation(&batch.translation_3d),
5366
                    // batch_range: 0..1,
5367
                    // extra_index: PhaseItemExtraIndex::NONE,
5368
                },
5369
                #[cfg(feature = "2d")]
5370
                PipelineMode::Camera3d,
5371
                ParticleRenderAlphaMaskPipelineKey::Opaque,
5372
            );
5373
        }
5374
    }
5375
}
5376

5377
/// Prepare GPU resources for effect rendering.
5378
///
5379
/// This system runs in the [`RenderSet::PrepareResources`] render set, after
5380
/// Bevy has updated the [`ViewUniforms`], which need to be referenced to get
5381
/// access to the current camera view.
5382
pub(crate) fn prepare_gpu_resources(
×
5383
    mut effects_meta: ResMut<EffectsMeta>,
5384
    //mut effect_cache: ResMut<EffectCache>,
5385
    mut event_cache: ResMut<EventCache>,
5386
    mut effect_bind_groups: ResMut<EffectBindGroups>,
5387
    mut sort_bind_groups: ResMut<SortBindGroups>,
5388
    render_device: Res<RenderDevice>,
5389
    render_queue: Res<RenderQueue>,
5390
    view_uniforms: Res<ViewUniforms>,
5391
    render_pipeline: Res<ParticlesRenderPipeline>,
5392
) {
5393
    // Get the binding for the ViewUniform, the uniform data structure containing
5394
    // the Camera data for the current view. If not available, we cannot render
5395
    // anything.
5396
    let Some(view_binding) = view_uniforms.uniforms.binding() else {
×
5397
        return;
×
5398
    };
5399

5400
    // Create the bind group for the camera/view parameters
5401
    // FIXME - Not here!
5402
    effects_meta.view_bind_group = Some(render_device.create_bind_group(
5403
        "hanabi:bind_group_camera_view",
5404
        &render_pipeline.view_layout,
5405
        &[
5406
            BindGroupEntry {
5407
                binding: 0,
5408
                resource: view_binding,
5409
            },
5410
            BindGroupEntry {
5411
                binding: 1,
5412
                resource: effects_meta.sim_params_uniforms.binding().unwrap(),
5413
            },
5414
        ],
5415
    ));
5416

5417
    // Re-/allocate any GPU buffer if needed
5418
    //effect_cache.prepare_buffers(&render_device, &render_queue, &mut
5419
    // effect_bind_groups);
5420
    event_cache.prepare_buffers(&render_device, &render_queue, &mut effect_bind_groups);
5421
    sort_bind_groups.prepare_buffers(&render_device);
5422
}
5423

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

5454
    // Once all GPU operations for this frame are enqueued, upload them to GPU
5455
    gpu_buffer_operations.end_frame(&render_device, &render_queue);
×
5456
}
5457

5458
pub(crate) fn prepare_bind_groups(
×
5459
    mut effects_meta: ResMut<EffectsMeta>,
5460
    mut effect_cache: ResMut<EffectCache>,
5461
    mut event_cache: ResMut<EventCache>,
5462
    mut effect_bind_groups: ResMut<EffectBindGroups>,
5463
    mut property_bind_groups: ResMut<PropertyBindGroups>,
5464
    mut sort_bind_groups: ResMut<SortBindGroups>,
5465
    property_cache: Res<PropertyCache>,
5466
    sorted_effect_batched: Res<SortedEffectBatches>,
5467
    render_device: Res<RenderDevice>,
5468
    dispatch_indirect_pipeline: Res<DispatchIndirectPipeline>,
5469
    utils_pipeline: Res<UtilsPipeline>,
5470
    update_pipeline: Res<ParticlesUpdatePipeline>,
5471
    render_pipeline: ResMut<ParticlesRenderPipeline>,
5472
    gpu_images: Res<RenderAssets<GpuImage>>,
5473
    mut gpu_buffer_operation_queue: ResMut<GpuBufferOperations>,
5474
) {
5475
    // We can't simulate nor render anything without at least the spawner buffer
5476
    if effects_meta.spawner_buffer.is_empty() {
×
5477
        return;
×
5478
    }
5479
    let Some(spawner_buffer) = effects_meta.spawner_buffer.buffer().cloned() else {
×
5480
        return;
×
5481
    };
5482

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

5488
    {
5489
        #[cfg(feature = "trace")]
5490
        let _span = bevy::utils::tracing::info_span!("shared_bind_groups").entered();
5491

5492
        // Make a copy of the buffer IDs before borrowing effects_meta mutably in the
5493
        // loop below. Also allows earlying out before doing any work in case some
5494
        // buffer is missing.
5495
        let Some(spawner_buffer) = effects_meta.spawner_buffer.buffer().cloned() else {
×
5496
            return;
×
5497
        };
5498

5499
        // Create the sim_params@0 bind group for the global simulation parameters,
5500
        // which is shared by the init and update passes.
5501
        if effects_meta.indirect_sim_params_bind_group.is_none() {
×
5502
            effects_meta.indirect_sim_params_bind_group = Some(render_device.create_bind_group(
×
5503
                "hanabi:bind_group:vfx_indirect:sim_params@0",
×
5504
                &update_pipeline.sim_params_layout, // FIXME - Shared with init
×
5505
                &[BindGroupEntry {
×
5506
                    binding: 0,
×
5507
                    resource: effects_meta.sim_params_uniforms.binding().unwrap(),
×
5508
                }],
5509
            ));
5510
        }
5511

5512
        // Create the @1 bind group for the indirect dispatch preparation pass of all
5513
        // effects at once
5514
        effects_meta.indirect_metadata_bind_group = match (
5515
            effects_meta.effect_metadata_buffer.buffer(),
5516
            effects_meta.update_dispatch_indirect_buffer.buffer(),
5517
        ) {
5518
            (Some(effect_metadata_buffer), Some(dispatch_indirect_buffer)) => {
×
5519
                // Base bind group for indirect pass
5520
                Some(render_device.create_bind_group(
×
5521
                    "hanabi:bind_group:vfx_indirect:metadata@1",
×
5522
                    &dispatch_indirect_pipeline.effect_metadata_bind_group_layout,
×
5523
                    &[
×
5524
                        // @group(1) @binding(0) var<storage, read_write> effect_metadata_buffer : array<u32>;
5525
                        BindGroupEntry {
×
5526
                            binding: 0,
×
5527
                            resource: BindingResource::Buffer(BufferBinding {
×
5528
                                buffer: effect_metadata_buffer,
×
5529
                                offset: 0,
×
5530
                                size: None, //NonZeroU64::new(256), // Some(GpuEffectMetadata::min_size()),
×
5531
                            }),
5532
                        },
5533
                        // @group(1) @binding(1) var<storage, read_write> dispatch_indirect_buffer : array<u32>;
5534
                        BindGroupEntry {
×
5535
                            binding: 1,
×
5536
                            resource: BindingResource::Buffer(BufferBinding {
×
5537
                                buffer: dispatch_indirect_buffer,
×
5538
                                offset: 0,
×
5539
                                size: None, //NonZeroU64::new(256), // Some(GpuDispatchIndirect::min_size()),
×
5540
                            }),
5541
                        },
5542
                    ],
5543
                ))
5544
            }
5545

5546
            // Some buffer is not yet available, can't create the bind group
5547
            _ => None,
×
5548
        };
5549

5550
        // Create the @2 bind group for the indirect dispatch preparation pass of all
5551
        // effects at once
5552
        if effects_meta.indirect_spawner_bind_group.is_none() {
×
5553
            let bind_group = render_device.create_bind_group(
×
5554
                "hanabi:bind_group:vfx_indirect:spawner@2",
5555
                &dispatch_indirect_pipeline.spawner_bind_group_layout,
×
5556
                &[
×
5557
                    // @group(2) @binding(0) var<storage, read> spawner_buffer : array<Spawner>;
5558
                    BindGroupEntry {
×
5559
                        binding: 0,
×
5560
                        resource: BindingResource::Buffer(BufferBinding {
×
5561
                            buffer: &spawner_buffer,
×
5562
                            offset: 0,
×
5563
                            size: None,
×
5564
                        }),
5565
                    },
5566
                ],
5567
            );
5568

5569
            effects_meta.indirect_spawner_bind_group = Some(bind_group);
×
5570
        }
5571
    }
5572

5573
    // Create the per-buffer bind groups
5574
    trace!("Create per-buffer bind groups...");
×
5575
    for (buffer_index, effect_buffer) in effect_cache.buffers().iter().enumerate() {
×
5576
        #[cfg(feature = "trace")]
5577
        let _span_buffer = bevy::utils::tracing::info_span!("create_buffer_bind_groups").entered();
5578

5579
        let Some(effect_buffer) = effect_buffer else {
×
5580
            trace!(
×
5581
                "Effect buffer index #{} has no allocated EffectBuffer, skipped.",
×
5582
                buffer_index
5583
            );
5584
            continue;
×
5585
        };
5586

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

5627
                BufferBindGroups { render }
×
5628
            });
5629
    }
5630

5631
    // Create bind groups for queued GPU buffer operations
5632
    gpu_buffer_operation_queue.create_bind_groups(&render_device, &utils_pipeline);
5633

5634
    // Create the per-effect bind groups
5635
    let spawner_buffer_binding_size =
5636
        NonZeroU64::new(effects_meta.spawner_buffer.aligned_size() as u64).unwrap();
5637
    for effect_batch in sorted_effect_batched.iter() {
×
5638
        #[cfg(feature = "trace")]
5639
        let _span_buffer = bevy::utils::tracing::info_span!("create_batch_bind_groups").entered();
×
5640

5641
        // Create the property bind group @2 if needed
5642
        if let Some(property_key) = &effect_batch.property_key {
×
5643
            if let Err(err) = property_bind_groups.ensure_exists(
×
5644
                property_key,
5645
                &property_cache,
5646
                &spawner_buffer,
5647
                spawner_buffer_binding_size,
5648
                &render_device,
5649
            ) {
5650
                error!("Failed to create property bind group for effect batch: {err:?}");
×
5651
                continue;
5652
            }
5653
        } else if let Err(err) = property_bind_groups.ensure_exists_no_property(
×
5654
            &property_cache,
×
5655
            &spawner_buffer,
×
5656
            spawner_buffer_binding_size,
×
5657
            &render_device,
×
5658
        ) {
5659
            error!("Failed to create property bind group for effect batch: {err:?}");
×
5660
            continue;
5661
        }
5662

5663
        // Bind group particle@1 for the simulate compute shaders (init and udpate) to
5664
        // simulate particles.
5665
        if effect_cache
×
5666
            .create_particle_sim_bind_group(
5667
                effect_batch.buffer_index,
×
5668
                &render_device,
×
5669
                effect_batch.particle_layout.min_binding_size32(),
×
5670
                effect_batch.parent_min_binding_size,
×
5671
                effect_batch.parent_binding_source.as_ref(),
×
5672
            )
5673
            .is_err()
5674
        {
5675
            error!("No particle buffer allocated for effect batch.");
×
5676
            continue;
×
5677
        }
5678

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

5725
        // Bind group @3 of update pass
5726
        // FIXME - this is instance-dependent, not buffer-dependent#
5727
        {
5728
            let num_event_buffers = effect_batch.child_event_buffers.len() as u32;
×
5729

5730
            let Some(update_metadata_layout) =
×
5731
                effect_cache.metadata_update_bind_group_layout(num_event_buffers)
5732
            else {
5733
                continue;
×
5734
            };
5735
            if effect_bind_groups
5736
                .get_or_create_update_metadata(
5737
                    effect_batch,
5738
                    &effects_meta.gpu_limits,
5739
                    &render_device,
5740
                    update_metadata_layout,
5741
                    effects_meta.effect_metadata_buffer.buffer().unwrap(),
5742
                    event_cache.child_infos_buffer(),
5743
                    &effect_batch.child_event_buffers[..],
5744
                )
5745
                .is_err()
5746
            {
5747
                continue;
×
5748
            }
5749
        }
5750

5751
        if effect_batch.layout_flags.contains(LayoutFlags::RIBBONS) {
×
5752
            let effect_buffer = effect_cache.get_buffer(effect_batch.buffer_index).unwrap();
×
5753

5754
            // Bind group @0 of sort-fill pass
5755
            let particle_buffer = effect_buffer.particle_buffer();
×
5756
            let indirect_index_buffer = effect_buffer.indirect_index_buffer();
×
5757
            let effect_metadata_buffer = effects_meta.effect_metadata_buffer.buffer().unwrap();
×
5758
            if let Err(err) = sort_bind_groups.ensure_sort_fill_bind_group(
×
5759
                &effect_batch.particle_layout,
×
5760
                particle_buffer,
×
5761
                indirect_index_buffer,
×
5762
                effect_metadata_buffer,
×
5763
            ) {
5764
                error!(
5765
                    "Failed to create sort-fill bind group @0 for ribbon effect: {:?}",
×
5766
                    err
5767
                );
5768
                continue;
5769
            }
5770

5771
            // Bind group @0 of sort-copy pass
5772
            let indirect_index_buffer = effect_buffer.indirect_index_buffer();
×
5773
            if let Err(err) = sort_bind_groups
×
5774
                .ensure_sort_copy_bind_group(indirect_index_buffer, effect_metadata_buffer)
×
5775
            {
5776
                error!(
5777
                    "Failed to create sort-copy bind group @0 for ribbon effect: {:?}",
×
5778
                    err
5779
                );
5780
                continue;
5781
            }
5782
        }
5783

5784
        // Ensure the particle texture(s) are available as GPU resources and that a bind
5785
        // group for them exists
5786
        // FIXME fix this insert+get below
5787
        if !effect_batch.texture_layout.layout.is_empty() {
×
5788
            // This should always be available, as this is cached into the render pipeline
5789
            // just before we start specializing it.
5790
            let Some(material_bind_group_layout) =
×
5791
                render_pipeline.get_material(&effect_batch.texture_layout)
×
5792
            else {
5793
                error!(
×
5794
                    "Failed to find material bind group layout for buffer #{}",
×
5795
                    effect_batch.buffer_index
5796
                );
5797
                continue;
×
5798
            };
5799

5800
            // TODO = move
5801
            let material = Material {
5802
                layout: effect_batch.texture_layout.clone(),
5803
                textures: effect_batch.textures.iter().map(|h| h.id()).collect(),
×
5804
            };
5805
            assert_eq!(material.layout.layout.len(), material.textures.len());
5806

5807
            //let bind_group_entries = material.make_entries(&gpu_images).unwrap();
5808
            let Ok(bind_group_entries) = material.make_entries(&gpu_images) else {
×
5809
                trace!(
×
5810
                    "Temporarily ignoring material {:?} due to missing image(s)",
×
5811
                    material
5812
                );
5813
                continue;
×
5814
            };
5815

5816
            effect_bind_groups
5817
                .material_bind_groups
5818
                .entry(material.clone())
5819
                .or_insert_with(|| {
×
5820
                    debug!("Creating material bind group for material {:?}", material);
×
5821
                    render_device.create_bind_group(
×
5822
                        &format!(
×
5823
                            "hanabi:material_bind_group_{}",
×
5824
                            material.layout.layout.len()
×
5825
                        )[..],
×
5826
                        material_bind_group_layout,
×
5827
                        &bind_group_entries[..],
×
5828
                    )
5829
                });
5830
        }
5831
    }
5832
}
5833

5834
type DrawEffectsSystemState = SystemState<(
5835
    SRes<EffectsMeta>,
5836
    SRes<EffectBindGroups>,
5837
    SRes<PipelineCache>,
5838
    SRes<RenderAssets<RenderMesh>>,
5839
    SRes<MeshAllocator>,
5840
    SQuery<Read<ViewUniformOffset>>,
5841
    SRes<SortedEffectBatches>,
5842
    SQuery<Read<EffectDrawBatch>>,
5843
)>;
5844

5845
/// Draw function for rendering all active effects for the current frame.
5846
///
5847
/// Effects are rendered in the [`Transparent2d`] phase of the main 2D pass,
5848
/// and the [`Transparent3d`] phase of the main 3D pass.
5849
pub(crate) struct DrawEffects {
5850
    params: DrawEffectsSystemState,
5851
}
5852

5853
impl DrawEffects {
5854
    pub fn new(world: &mut World) -> Self {
×
5855
        Self {
5856
            params: SystemState::new(world),
×
5857
        }
5858
    }
5859
}
5860

5861
/// Draw all particles of a single effect in view, in 2D or 3D.
5862
///
5863
/// FIXME: use pipeline ID to look up which group index it is.
5864
fn draw<'w>(
×
5865
    world: &'w World,
5866
    pass: &mut TrackedRenderPass<'w>,
5867
    view: Entity,
5868
    entity: (Entity, MainEntity),
5869
    pipeline_id: CachedRenderPipelineId,
5870
    params: &mut DrawEffectsSystemState,
5871
) {
5872
    let (
×
5873
        effects_meta,
×
5874
        effect_bind_groups,
×
5875
        pipeline_cache,
×
5876
        meshes,
×
5877
        mesh_allocator,
×
5878
        views,
×
5879
        sorted_effect_batches,
×
5880
        effect_draw_batches,
×
5881
    ) = params.get(world);
×
5882
    let view_uniform = views.get(view).unwrap();
×
5883
    let effects_meta = effects_meta.into_inner();
×
5884
    let effect_bind_groups = effect_bind_groups.into_inner();
×
5885
    let meshes = meshes.into_inner();
×
5886
    let mesh_allocator = mesh_allocator.into_inner();
×
5887
    let effect_draw_batch = effect_draw_batches.get(entity.0).unwrap();
×
5888
    let effect_batch = sorted_effect_batches
×
5889
        .get(effect_draw_batch.effect_batch_index)
×
5890
        .unwrap();
5891

5892
    let gpu_limits = &effects_meta.gpu_limits;
×
5893

5894
    let Some(pipeline) = pipeline_cache.into_inner().get_render_pipeline(pipeline_id) else {
×
5895
        return;
×
5896
    };
5897

5898
    trace!("render pass");
×
5899

5900
    pass.set_render_pipeline(pipeline);
×
5901

5902
    let Some(render_mesh): Option<&RenderMesh> = meshes.get(effect_batch.mesh) else {
×
5903
        return;
×
5904
    };
5905
    let Some(vertex_buffer_slice) = mesh_allocator.mesh_vertex_slice(&effect_batch.mesh) else {
×
5906
        return;
×
5907
    };
5908

5909
    // Vertex buffer containing the particle model to draw. Generally a quad.
5910
    // FIXME - need to upload "vertex_buffer_slice.range.start as i32" into
5911
    // "base_vertex" in the indirect struct...
5912
    assert_eq!(effect_batch.mesh_buffer_id, vertex_buffer_slice.buffer.id());
×
5913
    assert_eq!(effect_batch.mesh_slice, vertex_buffer_slice.range);
×
5914
    pass.set_vertex_buffer(0, vertex_buffer_slice.buffer.slice(..));
×
5915

5916
    // View properties (camera matrix, etc.)
5917
    pass.set_bind_group(
×
5918
        0,
5919
        effects_meta.view_bind_group.as_ref().unwrap(),
×
5920
        &[view_uniform.offset],
×
5921
    );
5922

5923
    // Particles buffer
5924
    let spawner_base = effect_batch.spawner_base;
×
5925
    let spawner_buffer_aligned = effects_meta.spawner_buffer.aligned_size();
×
5926
    assert!(spawner_buffer_aligned >= GpuSpawnerParams::min_size().get() as usize);
×
5927
    let spawner_offset = spawner_base * spawner_buffer_aligned as u32;
×
5928
    pass.set_bind_group(
×
5929
        1,
5930
        effect_bind_groups
×
5931
            .particle_render(effect_batch.buffer_index)
×
5932
            .unwrap(),
×
5933
        &[spawner_offset],
×
5934
    );
5935

5936
    // Particle texture
5937
    // TODO = move
5938
    let material = Material {
5939
        layout: effect_batch.texture_layout.clone(),
×
5940
        textures: effect_batch.textures.iter().map(|h| h.id()).collect(),
×
5941
    };
5942
    if !effect_batch.texture_layout.layout.is_empty() {
×
5943
        if let Some(bind_group) = effect_bind_groups.material_bind_groups.get(&material) {
×
5944
            pass.set_bind_group(2, bind_group, &[]);
×
5945
        } else {
5946
            // Texture(s) not ready; skip this drawing for now
5947
            trace!(
×
5948
                "Particle material bind group not available for batch buf={}. Skipping draw call.",
×
5949
                effect_batch.buffer_index,
×
5950
            );
5951
            return;
×
5952
        }
5953
    }
5954

5955
    let effect_metadata_index = effect_batch
×
5956
        .dispatch_buffer_indices
×
5957
        .effect_metadata_buffer_table_id
×
5958
        .0;
×
5959
    let effect_metadata_offset =
×
5960
        effect_metadata_index as u64 * gpu_limits.effect_metadata_aligned_size.get() as u64;
×
5961
    trace!(
×
5962
        "Draw up to {} particles with {} vertices per particle for batch from buffer #{} \
×
5963
            (effect_metadata_index={}, offset={}B).",
×
5964
        effect_batch.slice.len(),
×
5965
        render_mesh.vertex_count,
×
5966
        effect_batch.buffer_index,
×
5967
        effect_metadata_index,
×
5968
        effect_metadata_offset,
×
5969
    );
5970

5971
    // Note: the indirect draw args are the first few fields of GpuEffectMetadata
5972
    let Some(indirect_buffer) = effects_meta.effect_metadata_buffer.buffer() else {
×
5973
        trace!(
×
5974
            "The metadata buffer containing the indirect draw args is not ready for batch buf=#{}. Skipping draw call.",
×
5975
            effect_batch.buffer_index,
×
5976
        );
5977
        return;
×
5978
    };
5979

5980
    match render_mesh.buffer_info {
×
5981
        RenderMeshBufferInfo::Indexed { index_format, .. } => {
×
5982
            let Some(index_buffer_slice) = mesh_allocator.mesh_index_slice(&effect_batch.mesh)
×
5983
            else {
×
5984
                return;
×
5985
            };
5986

5987
            pass.set_index_buffer(index_buffer_slice.buffer.slice(..), 0, index_format);
×
5988
            pass.draw_indexed_indirect(indirect_buffer, effect_metadata_offset);
×
5989
        }
5990
        RenderMeshBufferInfo::NonIndexed => {
×
5991
            pass.draw_indirect(indirect_buffer, effect_metadata_offset);
×
5992
        }
5993
    }
5994
}
5995

5996
#[cfg(feature = "2d")]
5997
impl Draw<Transparent2d> for DrawEffects {
5998
    fn draw<'w>(
×
5999
        &mut self,
6000
        world: &'w World,
6001
        pass: &mut TrackedRenderPass<'w>,
6002
        view: Entity,
6003
        item: &Transparent2d,
6004
    ) -> Result<(), DrawError> {
6005
        trace!("Draw<Transparent2d>: view={:?}", view);
×
6006
        draw(
6007
            world,
×
6008
            pass,
×
6009
            view,
×
6010
            item.entity,
×
6011
            item.pipeline,
×
6012
            &mut self.params,
×
6013
        );
6014
        Ok(())
×
6015
    }
6016
}
6017

6018
#[cfg(feature = "3d")]
6019
impl Draw<Transparent3d> for DrawEffects {
6020
    fn draw<'w>(
×
6021
        &mut self,
6022
        world: &'w World,
6023
        pass: &mut TrackedRenderPass<'w>,
6024
        view: Entity,
6025
        item: &Transparent3d,
6026
    ) -> Result<(), DrawError> {
6027
        trace!("Draw<Transparent3d>: view={:?}", view);
×
6028
        draw(
6029
            world,
×
6030
            pass,
×
6031
            view,
×
6032
            item.entity,
×
6033
            item.pipeline,
×
6034
            &mut self.params,
×
6035
        );
6036
        Ok(())
×
6037
    }
6038
}
6039

6040
#[cfg(feature = "3d")]
6041
impl Draw<AlphaMask3d> for DrawEffects {
6042
    fn draw<'w>(
×
6043
        &mut self,
6044
        world: &'w World,
6045
        pass: &mut TrackedRenderPass<'w>,
6046
        view: Entity,
6047
        item: &AlphaMask3d,
6048
    ) -> Result<(), DrawError> {
6049
        trace!("Draw<AlphaMask3d>: view={:?}", view);
×
6050
        draw(
6051
            world,
×
6052
            pass,
×
6053
            view,
×
6054
            item.representative_entity,
×
6055
            item.key.pipeline,
×
6056
            &mut self.params,
×
6057
        );
6058
        Ok(())
×
6059
    }
6060
}
6061

6062
#[cfg(feature = "3d")]
6063
impl Draw<Opaque3d> for DrawEffects {
6064
    fn draw<'w>(
×
6065
        &mut self,
6066
        world: &'w World,
6067
        pass: &mut TrackedRenderPass<'w>,
6068
        view: Entity,
6069
        item: &Opaque3d,
6070
    ) -> Result<(), DrawError> {
6071
        trace!("Draw<Opaque3d>: view={:?}", view);
×
6072
        draw(
6073
            world,
×
6074
            pass,
×
6075
            view,
×
6076
            item.representative_entity,
×
6077
            item.key.pipeline,
×
6078
            &mut self.params,
×
6079
        );
6080
        Ok(())
×
6081
    }
6082
}
6083

6084
/// Render node to run the simulation sub-graph once per frame.
6085
///
6086
/// This node doesn't simulate anything by itself, but instead schedules the
6087
/// simulation sub-graph, where other nodes like [`VfxSimulateNode`] do the
6088
/// actual simulation.
6089
///
6090
/// The simulation sub-graph is scheduled to run before the [`CameraDriverNode`]
6091
/// renders all the views, such that rendered views have access to the
6092
/// just-simulated particles to render them.
6093
///
6094
/// [`CameraDriverNode`]: bevy::render::camera::CameraDriverNode
6095
pub(crate) struct VfxSimulateDriverNode;
6096

6097
impl Node for VfxSimulateDriverNode {
6098
    fn run(
×
6099
        &self,
6100
        graph: &mut RenderGraphContext,
6101
        _render_context: &mut RenderContext,
6102
        _world: &World,
6103
    ) -> Result<(), NodeRunError> {
6104
        graph.run_sub_graph(
×
6105
            crate::plugin::simulate_graph::HanabiSimulateGraph,
×
6106
            vec![],
×
6107
            None,
×
6108
        )?;
6109
        Ok(())
×
6110
    }
6111
}
6112

6113
#[derive(Debug, Clone, PartialEq, Eq)]
6114
enum HanabiPipelineId {
6115
    Invalid,
6116
    Cached(CachedComputePipelineId),
6117
}
6118

6119
pub(crate) enum ComputePipelineError {
6120
    Queued,
6121
    Creating,
6122
    Error,
6123
}
6124

6125
impl From<&CachedPipelineState> for ComputePipelineError {
6126
    fn from(value: &CachedPipelineState) -> Self {
×
6127
        match value {
×
6128
            CachedPipelineState::Queued => Self::Queued,
×
6129
            CachedPipelineState::Creating(_) => Self::Creating,
×
6130
            CachedPipelineState::Err(_) => Self::Error,
×
6131
            _ => panic!("Trying to convert Ok state to error."),
×
6132
        }
6133
    }
6134
}
6135

6136
pub(crate) struct HanabiComputePass<'a> {
6137
    /// Pipeline cache to fetch cached compute pipelines by ID.
6138
    pipeline_cache: &'a PipelineCache,
6139
    /// WGPU compute pass.
6140
    compute_pass: ComputePass<'a>,
6141
    /// Current pipeline (cached).
6142
    pipeline_id: HanabiPipelineId,
6143
}
6144

6145
impl<'a> Deref for HanabiComputePass<'a> {
6146
    type Target = ComputePass<'a>;
6147

6148
    fn deref(&self) -> &Self::Target {
×
6149
        &self.compute_pass
×
6150
    }
6151
}
6152

6153
impl DerefMut for HanabiComputePass<'_> {
6154
    fn deref_mut(&mut self) -> &mut Self::Target {
×
6155
        &mut self.compute_pass
×
6156
    }
6157
}
6158

6159
impl<'a> HanabiComputePass<'a> {
6160
    pub fn new(pipeline_cache: &'a PipelineCache, compute_pass: ComputePass<'a>) -> Self {
×
6161
        Self {
6162
            pipeline_cache,
6163
            compute_pass,
6164
            pipeline_id: HanabiPipelineId::Invalid,
6165
        }
6166
    }
6167

6168
    pub fn set_cached_compute_pipeline(
×
6169
        &mut self,
6170
        pipeline_id: CachedComputePipelineId,
6171
    ) -> Result<(), ComputePipelineError> {
6172
        trace!("set_cached_compute_pipeline() id={pipeline_id:?}");
×
6173
        if HanabiPipelineId::Cached(pipeline_id) == self.pipeline_id {
×
6174
            trace!("-> already set; skipped");
×
6175
            return Ok(());
×
6176
        }
6177
        let Some(pipeline) = self.pipeline_cache.get_compute_pipeline(pipeline_id) else {
×
6178
            let state = self.pipeline_cache.get_compute_pipeline_state(pipeline_id);
×
6179
            if let CachedPipelineState::Err(err) = state {
×
6180
                error!(
×
6181
                    "Failed to find compute pipeline #{}: {:?}",
×
6182
                    pipeline_id.id(),
×
6183
                    err
×
6184
                );
6185
            } else {
6186
                debug!("Compute pipeline not ready #{}", pipeline_id.id());
×
6187
            }
6188
            return Err(state.into());
×
6189
        };
6190
        self.compute_pass.set_pipeline(pipeline);
×
6191
        self.pipeline_id = HanabiPipelineId::Cached(pipeline_id);
×
6192
        Ok(())
×
6193
    }
6194
}
6195

6196
/// Render node to run the simulation of all effects once per frame.
6197
///
6198
/// Runs inside the simulation sub-graph, looping over all extracted effect
6199
/// batches to simulate them.
6200
pub(crate) struct VfxSimulateNode {}
6201

6202
impl VfxSimulateNode {
6203
    /// Create a new node for simulating the effects of the given world.
6204
    pub fn new(_world: &mut World) -> Self {
×
6205
        Self {}
6206
    }
6207

6208
    /// Begin a new compute pass and return a wrapper with extra
6209
    /// functionalities.
6210
    pub fn begin_compute_pass<'encoder>(
×
6211
        &self,
6212
        label: &str,
6213
        pipeline_cache: &'encoder PipelineCache,
6214
        render_context: &'encoder mut RenderContext,
6215
    ) -> HanabiComputePass<'encoder> {
6216
        let compute_pass =
×
6217
            render_context
×
6218
                .command_encoder()
6219
                .begin_compute_pass(&ComputePassDescriptor {
×
6220
                    label: Some(label),
×
6221
                    timestamp_writes: None,
×
6222
                });
6223
        HanabiComputePass::new(pipeline_cache, compute_pass)
×
6224
    }
6225
}
6226

6227
impl Node for VfxSimulateNode {
6228
    fn input(&self) -> Vec<SlotInfo> {
×
6229
        vec![]
×
6230
    }
6231

6232
    fn update(&mut self, _world: &mut World) {}
×
6233

6234
    fn run(
×
6235
        &self,
6236
        _graph: &mut RenderGraphContext,
6237
        render_context: &mut RenderContext,
6238
        world: &World,
6239
    ) -> Result<(), NodeRunError> {
6240
        trace!("VfxSimulateNode::run()");
×
6241

6242
        let pipeline_cache = world.resource::<PipelineCache>();
×
6243
        let effects_meta = world.resource::<EffectsMeta>();
×
6244
        let effect_bind_groups = world.resource::<EffectBindGroups>();
×
6245
        let property_bind_groups = world.resource::<PropertyBindGroups>();
×
6246
        let sort_bind_groups = world.resource::<SortBindGroups>();
×
6247
        let utils_pipeline = world.resource::<UtilsPipeline>();
×
6248
        let effect_cache = world.resource::<EffectCache>();
×
6249
        let event_cache = world.resource::<EventCache>();
×
6250
        let gpu_buffer_operations = world.resource::<GpuBufferOperations>();
×
6251
        let sorted_effect_batches = world.resource::<SortedEffectBatches>();
×
6252
        let init_fill_dispatch_queue = world.resource::<InitFillDispatchQueue>();
×
6253

6254
        // Make sure to schedule any buffer copy before accessing their content later in
6255
        // the GPU commands below.
6256
        {
6257
            let command_encoder = render_context.command_encoder();
×
6258
            effects_meta
×
6259
                .update_dispatch_indirect_buffer
×
6260
                .write_buffer(command_encoder);
×
6261
            effects_meta
×
6262
                .effect_metadata_buffer
×
6263
                .write_buffer(command_encoder);
×
6264
            sort_bind_groups.write_buffers(command_encoder);
×
6265
        }
6266

6267
        // Compute init fill dispatch pass - Fill the indirect dispatch structs for any
6268
        // upcoming init pass of this frame, based on the GPU spawn events emitted by
6269
        // the update pass of their parent effect during the previous frame.
6270
        if let Some(queue_index) = init_fill_dispatch_queue.submitted_queue_index.as_ref() {
×
6271
            gpu_buffer_operations.dispatch(
6272
                *queue_index,
6273
                render_context,
6274
                utils_pipeline,
6275
                Some("hanabi:init_indirect_fill_dispatch"),
6276
            );
6277
        }
6278

6279
        // If there's no batch, there's nothing more to do. Avoid continuing because
6280
        // some GPU resources are missing, which is expected when there's no effect but
6281
        // is an error (and will log warnings/errors) otherwise.
6282
        if sorted_effect_batches.is_empty() {
×
6283
            return Ok(());
×
6284
        }
6285

6286
        // Compute init pass
6287
        {
6288
            trace!("init: loop over effect batches...");
×
6289

6290
            let mut compute_pass =
6291
                self.begin_compute_pass("hanabi:init", pipeline_cache, render_context);
6292

6293
            // Bind group simparams@0 is common to everything, only set once per init pass
6294
            compute_pass.set_bind_group(
6295
                0,
6296
                effects_meta
6297
                    .indirect_sim_params_bind_group
6298
                    .as_ref()
6299
                    .unwrap(),
6300
                &[],
6301
            );
6302

6303
            // Dispatch init compute jobs for all batches
6304
            for effect_batch in sorted_effect_batches.iter() {
×
6305
                // Do not dispatch any init work if there's nothing to spawn this frame for the
6306
                // batch. Note that this hopefully should have been skipped earlier.
6307
                {
6308
                    let use_indirect_dispatch = effect_batch
×
6309
                        .layout_flags
×
6310
                        .contains(LayoutFlags::CONSUME_GPU_SPAWN_EVENTS);
×
6311
                    match effect_batch.spawn_info {
×
6312
                        BatchSpawnInfo::CpuSpawner { total_spawn_count } => {
×
6313
                            assert!(!use_indirect_dispatch);
×
6314
                            if total_spawn_count == 0 {
×
6315
                                continue;
×
6316
                            }
6317
                        }
6318
                        BatchSpawnInfo::GpuSpawner { .. } => {
6319
                            assert!(use_indirect_dispatch);
×
6320
                        }
6321
                    }
6322
                }
6323

6324
                // Fetch bind group particle@1
6325
                let Some(particle_bind_group) =
×
6326
                    effect_cache.particle_sim_bind_group(effect_batch.buffer_index)
×
6327
                else {
6328
                    error!(
×
6329
                        "Failed to find init particle@1 bind group for buffer index {}",
×
6330
                        effect_batch.buffer_index
6331
                    );
6332
                    continue;
×
6333
                };
6334

6335
                // Fetch bind group metadata@3
6336
                let Some(metadata_bind_group) = effect_bind_groups
×
6337
                    .init_metadata_bind_groups
6338
                    .get(&effect_batch.buffer_index)
6339
                else {
6340
                    error!(
×
6341
                        "Failed to find init metadata@3 bind group for buffer index {}",
×
6342
                        effect_batch.buffer_index
6343
                    );
6344
                    continue;
×
6345
                };
6346

6347
                if compute_pass
6348
                    .set_cached_compute_pipeline(effect_batch.init_and_update_pipeline_ids.init)
6349
                    .is_err()
6350
                {
6351
                    continue;
×
6352
                }
6353

6354
                // Compute dynamic offsets
NEW
6355
                let spawner_base = effect_batch.spawner_base;
×
6356
                let spawner_aligned_size = effects_meta.spawner_buffer.aligned_size();
×
NEW
6357
                debug_assert!(spawner_aligned_size >= GpuSpawnerParams::min_size().get() as usize);
×
NEW
6358
                let spawner_offset = spawner_base * spawner_aligned_size as u32;
×
UNCOV
6359
                let property_offset = effect_batch.property_offset;
×
6360

6361
                // Setup init pass
6362
                compute_pass.set_bind_group(1, particle_bind_group, &[]);
×
6363
                let offsets = if let Some(property_offset) = property_offset {
×
6364
                    vec![spawner_offset, property_offset]
6365
                } else {
6366
                    vec![spawner_offset]
×
6367
                };
6368
                compute_pass.set_bind_group(
×
6369
                    2,
6370
                    property_bind_groups
×
6371
                        .get(effect_batch.property_key.as_ref())
×
6372
                        .unwrap(),
×
6373
                    &offsets[..],
×
6374
                );
6375
                compute_pass.set_bind_group(3, &metadata_bind_group.bind_group, &[]);
×
6376

6377
                // Dispatch init job
6378
                match effect_batch.spawn_info {
×
6379
                    // Indirect dispatch via GPU spawn events
6380
                    BatchSpawnInfo::GpuSpawner {
6381
                        init_indirect_dispatch_index,
×
6382
                        ..
×
6383
                    } => {
×
6384
                        assert!(effect_batch
×
6385
                            .layout_flags
×
6386
                            .contains(LayoutFlags::CONSUME_GPU_SPAWN_EVENTS));
×
6387

6388
                        // Note: the indirect offset of a dispatch workgroup only needs
6389
                        // 4-byte alignment
6390
                        assert_eq!(GpuDispatchIndirect::min_size().get(), 12);
×
6391
                        let indirect_offset = init_indirect_dispatch_index as u64 * 12;
×
6392

6393
                        trace!(
×
6394
                            "record commands for indirect init pipeline of effect {:?} \
×
6395
                                init_indirect_dispatch_index={} \
×
6396
                                indirect_offset={} \
×
6397
                                spawner_base={} \
×
6398
                                spawner_offset={} \
×
6399
                                property_key={:?}...",
×
6400
                            effect_batch.handle,
6401
                            init_indirect_dispatch_index,
6402
                            indirect_offset,
6403
                            spawner_base,
6404
                            spawner_offset,
6405
                            effect_batch.property_key,
6406
                        );
6407

6408
                        compute_pass.dispatch_workgroups_indirect(
×
6409
                            event_cache.init_indirect_dispatch_buffer().unwrap(),
×
6410
                            indirect_offset,
×
6411
                        );
6412
                    }
6413

6414
                    // Direct dispatch via CPU spawn count
6415
                    BatchSpawnInfo::CpuSpawner {
6416
                        total_spawn_count: spawn_count,
×
6417
                    } => {
×
6418
                        assert!(!effect_batch
×
6419
                            .layout_flags
×
6420
                            .contains(LayoutFlags::CONSUME_GPU_SPAWN_EVENTS));
×
6421

6422
                        const WORKGROUP_SIZE: u32 = 64;
6423
                        let workgroup_count = spawn_count.div_ceil(WORKGROUP_SIZE);
×
6424

6425
                        trace!(
×
6426
                            "record commands for init pipeline of effect {:?} \
×
6427
                                (spawn {} particles => {} workgroups) spawner_base={} \
×
6428
                                spawner_offset={} \
×
6429
                                property_key={:?}...",
×
6430
                            effect_batch.handle,
6431
                            spawn_count,
6432
                            workgroup_count,
6433
                            spawner_base,
6434
                            spawner_offset,
6435
                            effect_batch.property_key,
6436
                        );
6437

6438
                        compute_pass.dispatch_workgroups(workgroup_count, 1, 1);
6439
                    }
6440
                }
6441

6442
                trace!("init compute dispatched");
×
6443
            }
6444
        }
6445

6446
        // Compute indirect dispatch pass
6447
        if effects_meta.spawner_buffer.buffer().is_some()
×
6448
            && !effects_meta.spawner_buffer.is_empty()
×
6449
            && effects_meta.indirect_metadata_bind_group.is_some()
×
6450
            && effects_meta.indirect_sim_params_bind_group.is_some()
×
6451
        {
6452
            // Only start a compute pass if there's an effect; makes things clearer in
6453
            // debugger.
6454
            let mut compute_pass =
×
6455
                self.begin_compute_pass("hanabi:indirect_dispatch", pipeline_cache, render_context);
×
6456

6457
            // Dispatch indirect dispatch compute job
6458
            trace!("record commands for indirect dispatch pipeline...");
×
6459

6460
            let has_gpu_spawn_events = !event_cache.child_infos().is_empty();
×
6461
            if has_gpu_spawn_events {
×
6462
                if let Some(indirect_child_info_buffer_bind_group) =
×
6463
                    event_cache.indirect_child_info_buffer_bind_group()
×
6464
                {
6465
                    assert!(has_gpu_spawn_events);
6466
                    compute_pass.set_bind_group(3, indirect_child_info_buffer_bind_group, &[]);
×
6467
                } else {
6468
                    error!("Missing child_info_buffer@3 bind group for the vfx_indirect pass.");
×
6469
                    render_context
×
6470
                        .command_encoder()
6471
                        .insert_debug_marker("ERROR:MissingIndirectBindGroup3");
6472
                    // FIXME - Bevy doesn't allow returning custom errors here...
6473
                    return Ok(());
×
6474
                }
6475
            }
6476

6477
            if compute_pass
×
6478
                .set_cached_compute_pipeline(effects_meta.active_indirect_pipeline_id)
×
6479
                .is_err()
6480
            {
6481
                // FIXME - Bevy doesn't allow returning custom errors here...
6482
                return Ok(());
×
6483
            }
6484

6485
            //error!("FIXME - effect_metadata_buffer has gaps!!!! this won't work. len() is
6486
            // the size exluding gaps!");
6487
            const WORKGROUP_SIZE: u32 = 64;
6488
            //let total_effect_count = effects_meta.effect_metadata_buffer.len();
6489
            let total_effect_count = effects_meta.spawner_buffer.len() as u32;
6490
            let workgroup_count = total_effect_count.div_ceil(WORKGROUP_SIZE);
6491

6492
            // Setup vfx_indirect pass
6493
            compute_pass.set_bind_group(
6494
                0,
6495
                effects_meta
6496
                    .indirect_sim_params_bind_group
6497
                    .as_ref()
6498
                    .unwrap(),
6499
                &[],
6500
            );
6501
            compute_pass.set_bind_group(
6502
                1,
6503
                // FIXME - got some unwrap() panic here, investigate... possibly race
6504
                // condition!
6505
                effects_meta.indirect_metadata_bind_group.as_ref().unwrap(),
6506
                &[],
6507
            );
6508
            compute_pass.set_bind_group(
6509
                2,
6510
                effects_meta.indirect_spawner_bind_group.as_ref().unwrap(),
6511
                &[],
6512
            );
6513
            compute_pass.dispatch_workgroups(workgroup_count, 1, 1);
6514
            trace!(
6515
                "indirect dispatch compute dispatched: total_effect_count={} workgroup_count={}",
×
6516
                total_effect_count,
6517
                workgroup_count
6518
            );
6519
        }
6520

6521
        // Compute update pass
6522
        {
6523
            let Some(indirect_buffer) = effects_meta.update_dispatch_indirect_buffer.buffer()
×
6524
            else {
6525
                warn!("Missing indirect buffer for update pass, cannot dispatch anything.");
×
6526
                render_context
×
6527
                    .command_encoder()
6528
                    .insert_debug_marker("ERROR:MissingUpdateIndirectBuffer");
6529
                // FIXME - Bevy doesn't allow returning custom errors here...
6530
                return Ok(());
×
6531
            };
6532

6533
            let mut compute_pass =
6534
                self.begin_compute_pass("hanabi:update", pipeline_cache, render_context);
6535

6536
            // Bind group simparams@0 is common to everything, only set once per update pass
6537
            compute_pass.set_bind_group(
6538
                0,
6539
                effects_meta
6540
                    .indirect_sim_params_bind_group
6541
                    .as_ref()
6542
                    .unwrap(),
6543
                &[],
6544
            );
6545

6546
            // Dispatch update compute jobs
6547
            for effect_batch in sorted_effect_batches.iter() {
×
6548
                // Fetch bind group particle@1
6549
                let Some(particle_bind_group) =
×
6550
                    effect_cache.particle_sim_bind_group(effect_batch.buffer_index)
×
6551
                else {
6552
                    error!(
×
6553
                        "Failed to find update particle@1 bind group for buffer index {}",
×
6554
                        effect_batch.buffer_index
6555
                    );
6556
                    continue;
×
6557
                };
6558

6559
                // Fetch bind group metadata@3
6560
                let Some(metadata_bind_group) = effect_bind_groups
×
6561
                    .update_metadata_bind_groups
6562
                    .get(&effect_batch.buffer_index)
6563
                else {
6564
                    error!(
×
6565
                        "Failed to find update metadata@3 bind group for buffer index {}",
×
6566
                        effect_batch.buffer_index
6567
                    );
6568
                    continue;
×
6569
                };
6570

6571
                // Fetch compute pipeline
6572
                if compute_pass
6573
                    .set_cached_compute_pipeline(effect_batch.init_and_update_pipeline_ids.update)
6574
                    .is_err()
6575
                {
6576
                    continue;
×
6577
                }
6578

6579
                // Compute dynamic offsets
6580
                let spawner_index = effect_batch.spawner_base;
×
6581
                let spawner_aligned_size = effects_meta.spawner_buffer.aligned_size();
×
6582
                assert!(spawner_aligned_size >= GpuSpawnerParams::min_size().get() as usize);
×
6583
                let spawner_offset = spawner_index * spawner_aligned_size as u32;
×
6584
                let property_offset = effect_batch.property_offset;
×
6585

6586
                trace!(
×
6587
                    "record commands for update pipeline of effect {:?} spawner_base={}",
×
6588
                    effect_batch.handle,
6589
                    spawner_index,
6590
                );
6591

6592
                // Setup update pass
6593
                compute_pass.set_bind_group(1, particle_bind_group, &[]);
6594
                let offsets = if let Some(property_offset) = property_offset {
×
6595
                    vec![spawner_offset, property_offset]
6596
                } else {
6597
                    vec![spawner_offset]
×
6598
                };
6599
                compute_pass.set_bind_group(
6600
                    2,
6601
                    property_bind_groups
6602
                        .get(effect_batch.property_key.as_ref())
6603
                        .unwrap(),
6604
                    &offsets[..],
6605
                );
6606
                compute_pass.set_bind_group(3, &metadata_bind_group.bind_group, &[]);
6607

6608
                // Dispatch update job
6609
                let dispatch_indirect_buffer_table_id = effect_batch
6610
                    .dispatch_buffer_indices
6611
                    .update_dispatch_indirect_buffer_table_id;
6612
                let dispatch_indirect_offset = dispatch_indirect_buffer_table_id.0 * 12;
6613
                trace!(
6614
                    "dispatch_workgroups_indirect: buffer={:?} offset={}B",
×
6615
                    indirect_buffer,
6616
                    dispatch_indirect_offset,
6617
                );
6618
                compute_pass
6619
                    .dispatch_workgroups_indirect(indirect_buffer, dispatch_indirect_offset as u64);
6620

6621
                trace!("update compute dispatched");
×
6622
            }
6623
        }
6624

6625
        // Compute sort fill dispatch pass - Fill the indirect dispatch structs for any
6626
        // batch of particles which needs sorting, based on the actual number of alive
6627
        // particles in the batch after their update in the compute update pass. Since
6628
        // particles may die during update, this may be different from the number of
6629
        // particles updated.
6630
        if let Some(queue_index) = sorted_effect_batches.dispatch_queue_index.as_ref() {
×
6631
            gpu_buffer_operations.dispatch(
6632
                *queue_index,
6633
                render_context,
6634
                utils_pipeline,
6635
                Some("hanabi:sort_fill_dispatch"),
6636
            );
6637
        }
6638

6639
        // Compute sort pass
6640
        {
6641
            let mut compute_pass =
6642
                self.begin_compute_pass("hanabi:sort", pipeline_cache, render_context);
6643

6644
            let effect_metadata_buffer = effects_meta.effect_metadata_buffer.buffer().unwrap();
6645
            let indirect_buffer = sort_bind_groups.indirect_buffer().unwrap();
6646

6647
            // Loop on batches and find those which need sorting
6648
            for effect_batch in sorted_effect_batches.iter() {
×
6649
                trace!("Processing effect batch for sorting...");
×
6650
                if !effect_batch.layout_flags.contains(LayoutFlags::RIBBONS) {
×
6651
                    continue;
×
6652
                }
6653
                assert!(effect_batch.particle_layout.contains(Attribute::RIBBON_ID));
×
6654
                assert!(effect_batch.particle_layout.contains(Attribute::AGE)); // or is that optional?
×
6655

6656
                let Some(effect_buffer) = effect_cache.get_buffer(effect_batch.buffer_index) else {
×
6657
                    warn!("Missing sort-fill effect buffer.");
×
6658
                    continue;
×
6659
                };
6660

6661
                let indirect_dispatch_index = *effect_batch
6662
                    .sort_fill_indirect_dispatch_index
6663
                    .as_ref()
6664
                    .unwrap();
6665
                let indirect_offset =
6666
                    sort_bind_groups.get_indirect_dispatch_byte_offset(indirect_dispatch_index);
6667

6668
                // Fill the sort buffer with the key-value pairs to sort
6669
                {
6670
                    compute_pass.push_debug_group("hanabi:sort_fill");
6671

6672
                    // Fetch compute pipeline
6673
                    let Some(pipeline_id) =
×
6674
                        sort_bind_groups.get_sort_fill_pipeline_id(&effect_batch.particle_layout)
6675
                    else {
6676
                        warn!("Missing sort-fill pipeline.");
×
6677
                        continue;
×
6678
                    };
6679
                    if compute_pass
6680
                        .set_cached_compute_pipeline(pipeline_id)
6681
                        .is_err()
6682
                    {
6683
                        compute_pass.pop_debug_group();
×
6684
                        // FIXME - Bevy doesn't allow returning custom errors here...
6685
                        return Ok(());
×
6686
                    }
6687

6688
                    // Bind group sort_fill@0
6689
                    let particle_buffer = effect_buffer.particle_buffer();
×
6690
                    let indirect_index_buffer = effect_buffer.indirect_index_buffer();
×
6691
                    let Some(bind_group) = sort_bind_groups.sort_fill_bind_group(
×
6692
                        particle_buffer.id(),
6693
                        indirect_index_buffer.id(),
6694
                        effect_metadata_buffer.id(),
6695
                    ) else {
6696
                        warn!("Missing sort-fill bind group.");
×
6697
                        continue;
×
6698
                    };
6699
                    let particle_offset = effect_buffer.particle_offset(effect_batch.slice.start);
6700
                    let indirect_index_offset =
6701
                        effect_buffer.indirect_index_offset(effect_batch.slice.start);
6702
                    let effect_metadata_offset = effects_meta.gpu_limits.effect_metadata_offset(
6703
                        effect_batch
6704
                            .dispatch_buffer_indices
6705
                            .effect_metadata_buffer_table_id
6706
                            .0,
6707
                    ) as u32;
6708
                    compute_pass.set_bind_group(
6709
                        0,
6710
                        bind_group,
6711
                        &[
6712
                            particle_offset,
6713
                            indirect_index_offset,
6714
                            effect_metadata_offset,
6715
                        ],
6716
                    );
6717

6718
                    compute_pass
6719
                        .dispatch_workgroups_indirect(indirect_buffer, indirect_offset as u64);
6720
                    trace!("Dispatched sort-fill with indirect offset +{indirect_offset}");
×
6721

6722
                    compute_pass.pop_debug_group();
6723
                }
6724

6725
                // Do the actual sort
6726
                {
6727
                    compute_pass.push_debug_group("hanabi:sort");
6728

6729
                    if compute_pass
6730
                        .set_cached_compute_pipeline(sort_bind_groups.sort_pipeline_id())
6731
                        .is_err()
6732
                    {
6733
                        compute_pass.pop_debug_group();
×
6734
                        // FIXME - Bevy doesn't allow returning custom errors here...
6735
                        return Ok(());
×
6736
                    }
6737

6738
                    compute_pass.set_bind_group(0, sort_bind_groups.sort_bind_group(), &[]);
×
6739
                    compute_pass
×
6740
                        .dispatch_workgroups_indirect(indirect_buffer, indirect_offset as u64);
×
6741
                    trace!("Dispatched sort with indirect offset +{indirect_offset}");
×
6742

6743
                    compute_pass.pop_debug_group();
6744
                }
6745

6746
                // Copy the sorted particle indices back into the indirect index buffer, where
6747
                // the render pass will read them.
6748
                {
6749
                    compute_pass.push_debug_group("hanabi:copy_sorted_indices");
6750

6751
                    // Fetch compute pipeline
6752
                    let pipeline_id = sort_bind_groups.get_sort_copy_pipeline_id();
6753
                    if compute_pass
6754
                        .set_cached_compute_pipeline(pipeline_id)
6755
                        .is_err()
6756
                    {
6757
                        compute_pass.pop_debug_group();
×
6758
                        // FIXME - Bevy doesn't allow returning custom errors here...
6759
                        return Ok(());
×
6760
                    }
6761

6762
                    // Bind group sort_copy@0
6763
                    let indirect_index_buffer = effect_buffer.indirect_index_buffer();
×
6764
                    let Some(bind_group) = sort_bind_groups.sort_copy_bind_group(
×
6765
                        indirect_index_buffer.id(),
6766
                        effect_metadata_buffer.id(),
6767
                    ) else {
6768
                        warn!("Missing sort-copy bind group.");
×
6769
                        continue;
×
6770
                    };
6771
                    let indirect_index_offset = effect_batch.slice.start;
6772
                    let effect_metadata_offset =
6773
                        effects_meta.effect_metadata_buffer.dynamic_offset(
6774
                            effect_batch
6775
                                .dispatch_buffer_indices
6776
                                .effect_metadata_buffer_table_id,
6777
                        );
6778
                    compute_pass.set_bind_group(
6779
                        0,
6780
                        bind_group,
6781
                        &[indirect_index_offset, effect_metadata_offset],
6782
                    );
6783

6784
                    compute_pass
6785
                        .dispatch_workgroups_indirect(indirect_buffer, indirect_offset as u64);
6786
                    trace!("Dispatched sort-copy with indirect offset +{indirect_offset}");
×
6787

6788
                    compute_pass.pop_debug_group();
6789
                }
6790
            }
6791
        }
6792

6793
        Ok(())
×
6794
    }
6795
}
6796

6797
impl From<LayoutFlags> for ParticleRenderAlphaMaskPipelineKey {
6798
    fn from(layout_flags: LayoutFlags) -> Self {
×
6799
        if layout_flags.contains(LayoutFlags::USE_ALPHA_MASK) {
×
6800
            ParticleRenderAlphaMaskPipelineKey::AlphaMask
×
6801
        } else if layout_flags.contains(LayoutFlags::OPAQUE) {
×
6802
            ParticleRenderAlphaMaskPipelineKey::Opaque
×
6803
        } else {
6804
            ParticleRenderAlphaMaskPipelineKey::Blend
×
6805
        }
6806
    }
6807
}
6808

6809
#[cfg(test)]
6810
mod tests {
6811
    use super::*;
6812

6813
    #[test]
6814
    fn layout_flags() {
6815
        let flags = LayoutFlags::default();
6816
        assert_eq!(flags, LayoutFlags::NONE);
6817
    }
6818

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

6824
        let renderer = MockRenderer::new();
6825
        let device = renderer.device();
6826
        let limits = GpuLimits::from_device(&device);
6827

6828
        // assert!(limits.storage_buffer_align().get() >= 1);
6829
        assert!(limits.effect_metadata_offset(256) >= 256 * GpuEffectMetadata::min_size().get());
6830
    }
6831

6832
    #[cfg(feature = "gpu_tests")]
6833
    #[test]
6834
    fn gpu_ops_ifda() {
6835
        use crate::test_utils::MockRenderer;
6836

6837
        let renderer = MockRenderer::new();
6838
        let device = renderer.device();
6839
        let render_queue = renderer.queue();
6840

6841
        let mut world = World::new();
6842
        world.insert_resource(device.clone());
6843
        let mut buffer_ops = GpuBufferOperations::from_world(&mut world);
6844

6845
        let src_buffer = device.create_buffer(&BufferDescriptor {
6846
            label: None,
6847
            size: 256,
6848
            usage: BufferUsages::STORAGE,
6849
            mapped_at_creation: false,
6850
        });
6851
        let dst_buffer = device.create_buffer(&BufferDescriptor {
6852
            label: None,
6853
            size: 256,
6854
            usage: BufferUsages::STORAGE,
6855
            mapped_at_creation: false,
6856
        });
6857

6858
        // Two consecutive ops can be merged. This includes having contiguous slices
6859
        // both in source and destination.
6860
        buffer_ops.begin_frame();
6861
        {
6862
            let mut q = InitFillDispatchQueue::default();
6863
            q.enqueue(0, 0);
6864
            assert_eq!(q.queue.len(), 1);
6865
            q.enqueue(1, 1);
6866
            // Ops are not batched yet
6867
            assert_eq!(q.queue.len(), 2);
6868
            // On submit, the ops get batched together
6869
            q.submit(&src_buffer, &dst_buffer, &mut buffer_ops);
6870
            assert_eq!(buffer_ops.args_buffer.len(), 1);
6871
        }
6872
        buffer_ops.end_frame(&device, &render_queue);
6873

6874
        // Even if out of order, the init fill dispatch ops are batchable. Here the
6875
        // offsets are enqueued inverted.
6876
        buffer_ops.begin_frame();
6877
        {
6878
            let mut q = InitFillDispatchQueue::default();
6879
            q.enqueue(1, 1);
6880
            assert_eq!(q.queue.len(), 1);
6881
            q.enqueue(0, 0);
6882
            // Ops are not batched yet
6883
            assert_eq!(q.queue.len(), 2);
6884
            // On submit, the ops get batched together
6885
            q.submit(&src_buffer, &dst_buffer, &mut buffer_ops);
6886
            assert_eq!(buffer_ops.args_buffer.len(), 1);
6887
        }
6888
        buffer_ops.end_frame(&device, &render_queue);
6889

6890
        // However, both the source and destination need to be contiguous at the same
6891
        // time. Here they are mixed so we can't batch.
6892
        buffer_ops.begin_frame();
6893
        {
6894
            let mut q = InitFillDispatchQueue::default();
6895
            q.enqueue(0, 1);
6896
            assert_eq!(q.queue.len(), 1);
6897
            q.enqueue(1, 0);
6898
            // Ops are not batched yet
6899
            assert_eq!(q.queue.len(), 2);
6900
            // On submit, the ops cannot get batched together
6901
            q.submit(&src_buffer, &dst_buffer, &mut buffer_ops);
6902
            assert_eq!(buffer_ops.args_buffer.len(), 2);
6903
        }
6904
        buffer_ops.end_frame(&device, &render_queue);
6905
    }
6906
}
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