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

djeedai / bevy_hanabi / 14652003207

24 Apr 2025 09:26PM UTC coverage: 39.884% (-0.1%) from 40.029%
14652003207

push

github

web-flow
Add support for Bevy 0.16 (#463)

5 of 73 new or added lines in 6 files covered. (6.85%)

2 existing lines in 2 files now uncovered.

3034 of 7607 relevant lines covered (39.88%)

17.63 hits per line

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

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

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

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

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

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

101
use self::batch::EffectBatch;
102

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

478
impl InitFillDispatchQueue {
479
    /// Clear the queue.
480
    #[inline]
481
    pub fn clear(&mut self) {
×
482
        self.queue.clear();
×
483
        self.submitted_queue_index = None;
×
484
    }
485

486
    /// Check if the queue is empty.
487
    #[inline]
488
    pub fn is_empty(&self) -> bool {
×
489
        self.queue.is_empty()
×
490
    }
491

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

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

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

517
        let mut fill_queue = GpuBufferOperationQueue::new();
518

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

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

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

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

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

634
        let storage_alignment = render_device.limits().min_storage_buffer_offset_alignment;
×
635
        let render_effect_metadata_size = GpuEffectMetadata::aligned_size(storage_alignment);
×
636
        let spawner_min_binding_size = GpuSpawnerParams::aligned_size(storage_alignment);
×
637

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

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

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

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

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

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

744
impl SpecializedComputePipeline for DispatchIndirectPipeline {
745
    type Key = DispatchIndirectPipelineKey;
746

747
    fn specialize(&self, key: Self::Key) -> ComputePipelineDescriptor {
×
748
        trace!(
×
749
            "Specializing indirect pipeline (has_events={})",
×
750
            key.has_events
751
        );
752

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

761
        let mut layout = Vec::with_capacity(4);
×
762
        layout.push(self.sim_params_bind_group_layout.clone());
×
763
        layout.push(self.effect_metadata_bind_group_layout.clone());
×
764
        layout.push(self.spawner_bind_group_layout.clone());
×
765
        if key.has_events {
×
766
            layout.push(self.child_infos_bind_group_layout.clone());
×
767
        }
768

769
        let label = format!(
×
770
            "hanabi:compute_pipeline:dispatch_indirect{}",
771
            if key.has_events {
×
772
                "_events"
×
773
            } else {
774
                "_noevent"
×
775
            }
776
        );
777

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1004
        // Upload to GPU buffer
1005
        self.args_buffer.write_buffer(device, render_queue);
3✔
1006
    }
1007

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

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

1105
        if queue.is_empty() {
×
1106
            return;
×
1107
        }
1108

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

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

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

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

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

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

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

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

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

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

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

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

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

1292
        let shader_code = include_str!("vfx_utils.wgsl");
×
1293

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

1299
            let shader_defs = default();
×
1300

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

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

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

1373
        Self {
1374
            bind_group_layout,
1375
            bind_group_layout_dyn,
1376
            bind_group_layout_no_src,
1377
            pipelines: [
×
1378
                zero_pipeline,
1379
                copy_pipeline,
1380
                fill_dispatch_args_pipeline,
1381
                fill_dispatch_args_self_pipeline,
1382
            ],
1383
        }
1384
    }
1385
}
1386

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

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

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

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

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

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

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

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

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

1491
impl SpecializedComputePipeline for ParticlesInitPipeline {
1492
    type Key = ParticleInitPipelineKey;
1493

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

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

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

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

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

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

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

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

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

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

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

1637
impl SpecializedComputePipeline for ParticlesUpdatePipeline {
1638
    type Key = ParticleUpdatePipelineKey;
1639

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

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

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

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

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

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

1717
impl ParticlesRenderPipeline {
1718
    /// Cache a material, creating its bind group layout based on the texture
1719
    /// layout.
1720
    pub fn cache_material(&mut self, layout: &TextureLayout) {
×
1721
        if layout.layout.is_empty() {
×
1722
            return;
×
1723
        }
1724

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

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

1764
        self.material_layouts
1765
            .insert(layout.clone(), material_bind_group_layout);
1766
    }
1767

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

1775
        self.material_layouts.get(layout)
×
1776
    }
1777
}
1778

1779
impl FromWorld for ParticlesRenderPipeline {
1780
    fn from_world(world: &mut World) -> Self {
×
1781
        let render_device = world.get_resource::<RenderDevice>().unwrap();
×
1782

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

1811
        Self {
1812
            render_device: render_device.clone(),
×
1813
            view_layout,
1814
            material_layouts: default(),
×
1815
        }
1816
    }
1817
}
1818

1819
#[cfg(all(feature = "2d", feature = "3d"))]
1820
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
1821
enum PipelineMode {
1822
    Camera2d,
1823
    Camera3d,
1824
}
1825

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

1868
#[derive(Clone, Copy, Default, Hash, PartialEq, Eq, Debug)]
1869
pub(crate) enum ParticleRenderAlphaMaskPipelineKey {
1870
    #[default]
1871
    Blend,
1872
    /// Key: USE_ALPHA_MASK
1873
    /// The effect is rendered with alpha masking.
1874
    AlphaMask,
1875
    /// Key: OPAQUE
1876
    /// The effect is rendered fully-opaquely.
1877
    Opaque,
1878
}
1879

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

1902
impl SpecializedRenderPipeline for ParticlesRenderPipeline {
1903
    type Key = ParticleRenderPipelineKey;
1904

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

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

1953
        let mut layout = vec![self.view_layout.clone(), particle_bind_group_layout];
×
1954
        let mut shader_defs = vec![];
×
1955

1956
        let vertex_buffer_layout = key.mesh_layout.as_ref().and_then(|mesh_layout| {
×
1957
            mesh_layout
×
1958
                .0
×
1959
                .get_layout(&[
×
1960
                    Mesh::ATTRIBUTE_POSITION.at_shader_location(0),
×
1961
                    Mesh::ATTRIBUTE_UV_0.at_shader_location(1),
×
1962
                    Mesh::ATTRIBUTE_NORMAL.at_shader_location(2),
×
1963
                ])
1964
                .ok()
×
1965
        });
1966

1967
        if let Some(material_bind_group_layout) = self.get_material(&key.texture_layout) {
×
1968
            layout.push(material_bind_group_layout.clone());
1969
        }
1970

1971
        // Key: LOCAL_SPACE_SIMULATION
1972
        if key.local_space_simulation {
×
1973
            shader_defs.push("LOCAL_SPACE_SIMULATION".into());
×
1974
        }
1975

1976
        match key.alpha_mask {
×
1977
            ParticleRenderAlphaMaskPipelineKey::Blend => {}
×
1978
            ParticleRenderAlphaMaskPipelineKey::AlphaMask => {
1979
                // Key: USE_ALPHA_MASK
1980
                shader_defs.push("USE_ALPHA_MASK".into())
×
1981
            }
1982
            ParticleRenderAlphaMaskPipelineKey::Opaque => {
1983
                // Key: OPAQUE
1984
                shader_defs.push("OPAQUE".into())
×
1985
            }
1986
        }
1987

1988
        // Key: FLIPBOOK
1989
        if key.flipbook {
×
1990
            shader_defs.push("FLIPBOOK".into());
×
1991
        }
1992

1993
        // Key: NEEDS_UV
1994
        if key.needs_uv {
×
1995
            shader_defs.push("NEEDS_UV".into());
×
1996
        }
1997

1998
        // Key: NEEDS_NORMAL
1999
        if key.needs_normal {
×
2000
            shader_defs.push("NEEDS_NORMAL".into());
×
2001
        }
2002

2003
        // Key: RIBBONS
2004
        if key.ribbons {
×
2005
            shader_defs.push("RIBBONS".into());
×
2006
        }
2007

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

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

2035
        #[cfg(all(feature = "2d", feature = "3d"))]
2036
        assert_eq!(CORE_2D_DEPTH_FORMAT, CORE_3D_DEPTH_FORMAT);
×
2037
        #[cfg(all(feature = "2d", feature = "3d"))]
2038
        let depth_stencil = match key.pipeline_mode {
×
2039
            PipelineMode::Camera2d => Some(depth_stencil_2d),
×
2040
            PipelineMode::Camera3d => Some(depth_stencil_3d),
×
2041
        };
2042

2043
        #[cfg(all(feature = "2d", not(feature = "3d")))]
2044
        let depth_stencil = Some(depth_stencil_2d);
2045

2046
        #[cfg(all(feature = "3d", not(feature = "2d")))]
2047
        let depth_stencil = Some(depth_stencil_3d);
2048

2049
        let format = if key.hdr {
×
2050
            ViewTarget::TEXTURE_FORMAT_HDR
×
2051
        } else {
2052
            TextureFormat::bevy_default()
×
2053
        };
2054

2055
        let hash = calc_func_id(&key);
×
2056
        let label = format!("hanabi:pipeline:render_{hash:016X}");
×
2057
        trace!(
×
2058
            "-> creating pipeline '{}' with shader defs:{}",
×
2059
            label,
×
2060
            shader_defs
×
2061
                .iter()
×
2062
                .fold(String::new(), |acc, x| acc + &format!(" {x:?}"))
×
2063
        );
2064

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

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

2157
pub struct AddedEffectParent {
2158
    pub entity: MainEntity,
2159
    pub layout: ParticleLayout,
2160
    /// GPU spawn event count to allocate for this effect.
2161
    pub event_count: u32,
2162
}
2163

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

2193
/// Collection of all extracted effects for this frame, inserted into the
2194
/// render world as a render resource.
2195
#[derive(Default, Resource)]
2196
pub(crate) struct ExtractedEffects {
2197
    /// Extracted effects this frame.
2198
    pub effects: Vec<ExtractedEffect>,
2199
    /// Newly added effects without a GPU allocation yet.
2200
    pub added_effects: Vec<AddedEffect>,
2201
}
2202

2203
#[derive(Default, Resource)]
2204
pub(crate) struct EffectAssetEvents {
2205
    pub images: Vec<AssetEvent<Image>>,
2206
}
2207

2208
/// System extracting all the asset events for the [`Image`] assets to enable
2209
/// dynamic update of images bound to any effect.
2210
///
2211
/// This system runs in parallel of [`extract_effects`].
2212
pub(crate) fn extract_effect_events(
×
2213
    mut events: ResMut<EffectAssetEvents>,
2214
    mut image_events: Extract<EventReader<AssetEvent<Image>>>,
2215
) {
2216
    #[cfg(feature = "trace")]
NEW
2217
    let _span = bevy::log::info_span!("extract_effect_events").entered();
×
2218
    trace!("extract_effect_events()");
×
2219

2220
    let EffectAssetEvents { ref mut images } = *events;
×
2221
    *images = image_events.read().copied().collect();
×
2222
}
2223

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

2258
    /// Enable automatically starting a GPU debugger capture when one or more
2259
    /// effects are spawned.
2260
    ///
2261
    /// Enable this feature to automatically capture one or more GPU frames when
2262
    /// a new effect is spawned (as detected by ECS change detection). This
2263
    /// instructs any attached GPU debugger to start a capture; this has no
2264
    /// effect if no debugger is attached.
2265
    pub start_capture_on_new_effect: bool,
2266

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

2281
#[derive(Debug, Default, Clone, Copy, Resource)]
2282
pub(crate) struct RenderDebugSettings {
2283
    /// Is a GPU debugger capture on-going?
2284
    is_capturing: bool,
2285
    /// Start time of any on-going GPU debugger capture.
2286
    capture_start: Duration,
2287
    /// Number of frames captured so far for on-going GPU debugger capture.
2288
    captured_frames: u32,
2289
}
2290

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

2343
    // Manage GPU debug capture
2344
    if render_debug_settings.is_capturing {
×
2345
        render_debug_settings.captured_frames += 1;
×
2346

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

2374
    // Save simulation params into render world
2375
    sim_params.time = time.elapsed_secs_f64();
×
2376
    sim_params.delta_time = time.delta_secs();
×
2377
    sim_params.virtual_time = virtual_time.elapsed_secs_f64();
×
2378
    sim_params.virtual_delta_time = virtual_time.delta_secs();
×
2379
    sim_params.real_time = real_time.elapsed_secs_f64();
×
2380
    sim_params.real_delta_time = real_time.delta_secs();
×
2381

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

2415
            trace!(
×
2416
                "Found new effect: entity {:?} | render entity {:?} | capacity {:?} | particle_layout {:?} | \
×
2417
                 property_layout {:?} | layout_flags {:?} | mesh {:?}",
×
2418
                 entity,
×
2419
                 render_entity.id(),
×
2420
                 asset.capacity(),
×
2421
                 particle_layout,
2422
                 property_layout,
2423
                 compiled_effect.layout_flags,
2424
                 mesh);
2425

2426
            // 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
2427
            const FIXME_HARD_CODED_EVENT_COUNT: u32 = 256;
2428
            let parent = compiled_effect.parent.map(|entity| AddedEffectParent {
×
2429
                entity: entity.into(),
×
2430
                layout: compiled_effect.parent_particle_layout.as_ref().unwrap().clone(),
×
2431
                event_count: FIXME_HARD_CODED_EVENT_COUNT,
×
2432
            });
2433

2434
            trace!("Found new effect: entity {:?} | capacity {:?} | particle_layout {:?} | property_layout {:?} | layout_flags {:?}", entity, asset.capacity(), particle_layout, property_layout, compiled_effect.layout_flags);
×
2435
            Some(AddedEffect {
×
2436
                entity: MainEntity::from(entity),
×
2437
                render_entity: *render_entity,
×
2438
                capacity: asset.capacity(),
×
2439
                mesh,
×
2440
                parent,
×
2441
                particle_layout,
×
2442
                property_layout,
×
2443
                layout_flags: compiled_effect.layout_flags,
×
2444
                handle,
×
2445
            })
2446
        })
2447
        .collect();
×
2448

2449
    // Loop over all existing effects to extract them
2450
    extracted_effects.effects.clear();
×
2451
    for (
2452
        main_entity,
×
2453
        render_entity,
×
2454
        maybe_inherited_visibility,
×
2455
        maybe_view_visibility,
×
2456
        effect_spawner,
×
2457
        compiled_effect,
×
2458
        maybe_properties,
×
2459
        transform,
×
2460
    ) in q_effects.iter()
×
2461
    {
2462
        // Check if shaders are configured
2463
        let Some(effect_shaders) = compiled_effect.get_configured_shaders() else {
×
2464
            continue;
×
2465
        };
2466

2467
        // Check if hidden, unless always simulated
2468
        if compiled_effect.simulation_condition == SimulationCondition::WhenVisible
2469
            && !maybe_inherited_visibility
×
2470
                .map(|cv| cv.get())
×
2471
                .unwrap_or(true)
×
2472
            && !maybe_view_visibility.map(|cv| cv.get()).unwrap_or(true)
×
2473
        {
2474
            continue;
×
2475
        }
2476

2477
        // Check if asset is available, otherwise silently ignore
2478
        let Some(asset) = effects.get(&compiled_effect.asset) else {
×
2479
            trace!(
×
2480
                "EffectAsset not ready; skipping ParticleEffect instance on entity {:?}.",
×
2481
                main_entity
2482
            );
2483
            continue;
×
2484
        };
2485

2486
        // Resolve the render entity of the parent, if any
2487
        let _parent = if let Some(main_entity) = compiled_effect.parent {
×
2488
            let Ok((_, render_entity, _, _, _, _, _, _)) = q_effects.get(main_entity) else {
×
2489
                error!(
×
2490
                    "Failed to resolve render entity of parent with main entity {:?}.",
×
2491
                    main_entity
2492
                );
2493
                continue;
×
2494
            };
2495
            Some(*render_entity)
2496
        } else {
2497
            None
×
2498
        };
2499

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

2516
        let texture_layout = asset.module().texture_layout();
2517
        let layout_flags = compiled_effect.layout_flags;
2518
        // let mesh = compiled_effect
2519
        //     .mesh
2520
        //     .clone()
2521
        //     .unwrap_or(default_mesh.0.clone());
2522
        let alpha_mode = compiled_effect.alpha_mode;
2523

2524
        trace!(
2525
            "Extracted instance of effect '{}' on entity {:?} (render entity {:?}): texture_layout_count={} texture_count={} layout_flags={:?}",
×
2526
            asset.name,
×
2527
            main_entity,
×
2528
            render_entity.id(),
×
2529
            texture_layout.layout.len(),
×
2530
            compiled_effect.textures.len(),
×
2531
            layout_flags,
2532
        );
2533

2534
        extracted_effects.effects.push(ExtractedEffect {
2535
            render_entity: *render_entity,
2536
            main_entity: main_entity.into(),
2537
            handle: compiled_effect.asset.clone_weak(),
2538
            particle_layout: asset.particle_layout().clone(),
2539
            property_layout,
2540
            property_data,
2541
            spawn_count: effect_spawner.spawn_count,
2542
            prng_seed: compiled_effect.prng_seed,
2543
            transform: *transform,
2544
            layout_flags,
2545
            texture_layout,
2546
            textures: compiled_effect.textures.clone(),
2547
            alpha_mode,
2548
            effect_shaders: effect_shaders.clone(),
2549
        });
2550
    }
2551
}
2552

2553
/// Various GPU limits and aligned sizes computed once and cached.
2554
struct GpuLimits {
2555
    /// Value of [`WgpuLimits::min_storage_buffer_offset_alignment`].
2556
    ///
2557
    /// [`WgpuLimits::min_storage_buffer_offset_alignment`]: bevy::render::settings::WgpuLimits::min_storage_buffer_offset_alignment
2558
    storage_buffer_align: NonZeroU32,
2559

2560
    /// Size of [`GpuEffectMetadata`] aligned to the contraint of
2561
    /// [`WgpuLimits::min_storage_buffer_offset_alignment`].
2562
    ///
2563
    /// [`WgpuLimits::min_storage_buffer_offset_alignment`]: bevy::render::settings::WgpuLimits::min_storage_buffer_offset_alignment
2564
    effect_metadata_aligned_size: NonZeroU32,
2565
}
2566

2567
impl GpuLimits {
2568
    pub fn from_device(render_device: &RenderDevice) -> Self {
1✔
2569
        let storage_buffer_align =
1✔
2570
            render_device.limits().min_storage_buffer_offset_alignment as u64;
1✔
2571

2572
        let effect_metadata_aligned_size = NonZeroU32::new(
2573
            GpuEffectMetadata::min_size()
1✔
2574
                .get()
1✔
2575
                .next_multiple_of(storage_buffer_align) as u32,
1✔
2576
        )
2577
        .unwrap();
2578

2579
        trace!(
1✔
2580
            "GPU-aligned sizes (align: {} B):\n- GpuEffectMetadata: {} B -> {} B",
×
2581
            storage_buffer_align,
×
2582
            GpuEffectMetadata::min_size().get(),
×
2583
            effect_metadata_aligned_size.get(),
×
2584
        );
2585

2586
        Self {
2587
            storage_buffer_align: NonZeroU32::new(storage_buffer_align as u32).unwrap(),
1✔
2588
            effect_metadata_aligned_size,
2589
        }
2590
    }
2591

2592
    /// Byte alignment for any storage buffer binding.
2593
    pub fn storage_buffer_align(&self) -> NonZeroU32 {
×
2594
        self.storage_buffer_align
×
2595
    }
2596

2597
    /// Byte offset of the [`GpuEffectMetadata`] of a given buffer.
2598
    pub fn effect_metadata_offset(&self, buffer_index: u32) -> u64 {
1✔
2599
        self.effect_metadata_aligned_size.get() as u64 * buffer_index as u64
1✔
2600
    }
2601

2602
    /// Byte alignment for [`GpuEffectMetadata`].
2603
    pub fn effect_metadata_size(&self) -> NonZeroU64 {
×
2604
        NonZeroU64::new(self.effect_metadata_aligned_size.get() as u64).unwrap()
×
2605
    }
2606
}
2607

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

2654
impl EffectsMeta {
2655
    pub fn new(
×
2656
        device: RenderDevice,
2657
        indirect_shader_noevent: Handle<Shader>,
2658
        indirect_shader_events: Handle<Shader>,
2659
    ) -> Self {
2660
        let gpu_limits = GpuLimits::from_device(&device);
×
2661

2662
        // Ensure individual GpuSpawnerParams elements are properly aligned so they can
2663
        // be addressed individually by the computer shaders.
2664
        let item_align = gpu_limits.storage_buffer_align().get() as u64;
×
2665
        trace!(
×
2666
            "Aligning storage buffers to {} bytes as device limits requires.",
×
2667
            item_align
2668
        );
2669

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

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

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

2727
            // Allocate an indirect dispatch arguments struct for this instance
2728
            let update_dispatch_indirect_buffer_row_index =
2729
                self.update_dispatch_indirect_buffer.allocate();
2730

2731
            // Allocate per-effect metadata.
2732
            let gpu_effect_metadata = GpuEffectMetadata {
2733
                alive_count: 0,
2734
                max_update: 0,
2735
                dead_count: added_effect.capacity,
2736
                max_spawn: added_effect.capacity,
2737
                ..default()
2738
            };
2739
            trace!("+ Effect: {:?}", gpu_effect_metadata);
×
2740
            let effect_metadata_buffer_table_id =
2741
                self.effect_metadata_buffer.insert(gpu_effect_metadata);
2742
            let dispatch_buffer_indices = DispatchBufferIndices {
2743
                update_dispatch_indirect_buffer_row_index,
2744
                effect_metadata_buffer_table_id,
2745
            };
2746

2747
            // Insert the effect into the cache. This will allocate all the necessary
2748
            // mandatory GPU resources as needed.
2749
            let cached_effect = effect_cache.insert(
2750
                added_effect.handle,
2751
                added_effect.capacity,
2752
                &added_effect.particle_layout,
2753
                added_effect.layout_flags,
2754
            );
2755
            let mut cmd = commands.entity(added_effect.render_entity.id());
2756
            cmd.insert((
2757
                added_effect.entity,
2758
                cached_effect,
2759
                dispatch_buffer_indices,
2760
                CachedMesh {
2761
                    mesh: added_effect.mesh.id(),
2762
                },
2763
            ));
2764

2765
            // Allocate storage for properties if needed
2766
            if !added_effect.property_layout.is_empty() {
×
2767
                let cached_effect_properties = property_cache.insert(&added_effect.property_layout);
×
2768
                cmd.insert(cached_effect_properties);
×
2769
            } else {
2770
                cmd.remove::<CachedEffectProperties>();
×
2771
            }
2772

2773
            // Allocate storage for the reference to the parent effect if needed. Note that
2774
            // we cannot yet allocate the complete parent info (CachedChildInfo) because it
2775
            // depends on the list of children, which we can't resolve until all
2776
            // effects have been added/removed this frame. This will be done later in
2777
            // resolve_parents().
2778
            if let Some(parent) = added_effect.parent.as_ref() {
×
2779
                let cached_parent: CachedParentRef = CachedParentRef {
2780
                    entity: parent.entity,
2781
                };
2782
                cmd.insert(cached_parent);
2783
                trace!("+ new effect declares parent entity {:?}", parent.entity);
×
2784
            } else {
2785
                cmd.remove::<CachedParentRef>();
×
2786
                trace!("+ new effect declares no parent");
×
2787
            }
2788

2789
            // Allocate storage for GPU spawn events if needed
2790
            if let Some(parent) = added_effect.parent.as_ref() {
×
2791
                let cached_events = event_cache.allocate(parent.event_count);
2792
                cmd.insert(cached_events);
2793
            } else {
2794
                cmd.remove::<CachedEffectEvents>();
×
2795
            }
2796

2797
            // Ensure the particle@1 bind group layout exists for the given configuration of
2798
            // particle layout and (optionally) parent particle layout.
2799
            {
2800
                let parent_min_binding_size = added_effect
2801
                    .parent
2802
                    .map(|added_parent| added_parent.layout.min_binding_size32());
×
2803
                effect_cache.ensure_particle_bind_group_layout(
2804
                    added_effect.particle_layout.min_binding_size32(),
2805
                    parent_min_binding_size,
2806
                );
2807
            }
2808

2809
            // Ensure the metadata@3 bind group layout exists for init pass.
2810
            {
2811
                let consume_gpu_spawn_events = added_effect
2812
                    .layout_flags
2813
                    .contains(LayoutFlags::CONSUME_GPU_SPAWN_EVENTS);
2814
                effect_cache.ensure_metadata_init_bind_group_layout(consume_gpu_spawn_events);
2815
            }
2816

2817
            // We cannot yet determine the layout of the metadata@3 bind group for the
2818
            // update pass, because it depends on the number of children, and
2819
            // this is encoded indirectly via the number of child effects
2820
            // pointing to this parent, and only calculated later in
2821
            // resolve_parents().
2822

2823
            trace!(
2824
                "+ added effect entity {:?}: main_entity={:?} \
×
2825
                first_update_group_dispatch_buffer_index={} \
×
2826
                render_effect_dispatch_buffer_id={}",
×
2827
                added_effect.render_entity,
2828
                added_effect.entity,
2829
                update_dispatch_indirect_buffer_row_index,
2830
                effect_metadata_buffer_table_id.0
2831
            );
2832
        }
2833
    }
2834

2835
    pub fn allocate_spawner(
×
2836
        &mut self,
2837
        global_transform: &GlobalTransform,
2838
        spawn_count: u32,
2839
        prng_seed: u32,
2840
        effect_metadata_buffer_table_id: BufferTableId,
2841
    ) -> u32 {
2842
        let spawner_base = self.spawner_buffer.len() as u32;
×
2843
        let transform = global_transform.compute_matrix().into();
×
2844
        let inverse_transform = Mat4::from(
2845
            // Inverse the Affine3A first, then convert to Mat4. This is a lot more
2846
            // efficient than inversing the Mat4.
2847
            global_transform.affine().inverse(),
×
2848
        )
2849
        .into();
2850
        let spawner_params = GpuSpawnerParams {
2851
            transform,
2852
            inverse_transform,
2853
            spawn: spawn_count as i32,
×
2854
            seed: prng_seed,
2855
            effect_metadata_index: effect_metadata_buffer_table_id.0,
×
2856
            ..default()
2857
        };
2858
        trace!("spawner params = {:?}", spawner_params);
×
2859
        self.spawner_buffer.push(spawner_params);
×
2860
        spawner_base
×
2861
    }
2862
}
2863

2864
bitflags! {
2865
    /// Effect flags.
2866
    #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
2867
    pub struct LayoutFlags: u32 {
2868
        /// No flags.
2869
        const NONE = 0;
2870
        // DEPRECATED - The effect uses an image texture.
2871
        //const PARTICLE_TEXTURE = (1 << 0);
2872
        /// The effect is simulated in local space.
2873
        const LOCAL_SPACE_SIMULATION = (1 << 2);
2874
        /// The effect uses alpha masking instead of alpha blending. Only used for 3D.
2875
        const USE_ALPHA_MASK = (1 << 3);
2876
        /// The effect is rendered with flipbook texture animation based on the
2877
        /// [`Attribute::SPRITE_INDEX`] of each particle.
2878
        const FLIPBOOK = (1 << 4);
2879
        /// The effect needs UVs.
2880
        const NEEDS_UV = (1 << 5);
2881
        /// The effect has ribbons.
2882
        const RIBBONS = (1 << 6);
2883
        /// The effects needs normals.
2884
        const NEEDS_NORMAL = (1 << 7);
2885
        /// The effect is fully-opaque.
2886
        const OPAQUE = (1 << 8);
2887
        /// The (update) shader emits GPU spawn events to instruct another effect to spawn particles.
2888
        const EMIT_GPU_SPAWN_EVENTS = (1 << 9);
2889
        /// The (init) shader spawns particles by consuming GPU spawn events, instead of
2890
        /// a single CPU spawn count.
2891
        const CONSUME_GPU_SPAWN_EVENTS = (1 << 10);
2892
        /// The (init or update) shader needs access to its parent particle. This allows
2893
        /// a particle init or update pass to read the data of a parent particle, for
2894
        /// example to inherit some of the attributes.
2895
        const READ_PARENT_PARTICLE = (1 << 11);
2896
    }
2897
}
2898

2899
impl Default for LayoutFlags {
2900
    fn default() -> Self {
1✔
2901
        Self::NONE
1✔
2902
    }
2903
}
2904

2905
/// Observer raised when the [`CachedEffect`] component is removed, which
2906
/// indicates that the effect instance was despawned.
2907
pub(crate) fn on_remove_cached_effect(
×
2908
    trigger: Trigger<OnRemove, CachedEffect>,
2909
    query: Query<(
2910
        Entity,
2911
        MainEntity,
2912
        &CachedEffect,
2913
        &DispatchBufferIndices,
2914
        Option<&CachedEffectProperties>,
2915
        Option<&CachedParentInfo>,
2916
        Option<&CachedEffectEvents>,
2917
    )>,
2918
    mut effect_cache: ResMut<EffectCache>,
2919
    mut effect_bind_groups: ResMut<EffectBindGroups>,
2920
    mut effects_meta: ResMut<EffectsMeta>,
2921
    mut event_cache: ResMut<EventCache>,
2922
) {
2923
    #[cfg(feature = "trace")]
NEW
2924
    let _span = bevy::log::info_span!("on_remove_cached_effect").entered();
×
2925

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

2929
    // Fecth the components of the effect being destroyed. Note that the despawn
2930
    // command above is not yet applied, so this query should always succeed.
2931
    let Ok((
2932
        render_entity,
×
2933
        main_entity,
×
2934
        cached_effect,
×
2935
        dispatch_buffer_indices,
×
2936
        _opt_props,
×
2937
        _opt_parent,
×
2938
        opt_cached_effect_events,
×
NEW
2939
    )) = query.get(trigger.target())
×
2940
    else {
2941
        return;
×
2942
    };
2943

2944
    // Dealllocate the effect slice in the event buffer, if any.
2945
    if let Some(cached_effect_events) = opt_cached_effect_events {
×
2946
        match event_cache.free(cached_effect_events) {
2947
            Err(err) => {
×
2948
                error!("Error while freeing effect event slice: {err:?}");
×
2949
            }
2950
            Ok(buffer_state) => {
×
2951
                if buffer_state != BufferState::Used {
×
2952
                    // Clear bind groups associated with the old buffer
2953
                    effect_bind_groups.init_metadata_bind_groups.clear();
×
2954
                    effect_bind_groups.update_metadata_bind_groups.clear();
×
2955
                }
2956
            }
2957
        }
2958
    }
2959

2960
    // Deallocate the effect slice in the GPU effect buffer, and if this was the
2961
    // last slice, also deallocate the GPU buffer itself.
2962
    trace!(
2963
        "=> ParticleEffect on render entity {:?} associated with main entity {:?}, removing...",
×
2964
        render_entity,
2965
        main_entity,
2966
    );
2967
    let Ok(BufferState::Free) = effect_cache.remove(cached_effect) else {
2968
        // Buffer was not affected, so all bind groups are still valid. Nothing else to
2969
        // do.
2970
        return;
×
2971
    };
2972

2973
    // Clear bind groups associated with the removed buffer
2974
    trace!(
×
2975
        "=> GPU buffer #{} gone, destroying its bind groups...",
×
2976
        cached_effect.buffer_index
2977
    );
2978
    effect_bind_groups
2979
        .particle_buffers
2980
        .remove(&cached_effect.buffer_index);
2981
    effects_meta
2982
        .update_dispatch_indirect_buffer
2983
        .free(dispatch_buffer_indices.update_dispatch_indirect_buffer_row_index);
2984
    effects_meta
2985
        .effect_metadata_buffer
2986
        .remove(dispatch_buffer_indices.effect_metadata_buffer_table_id);
2987
}
2988

2989
/// Update the [`CachedEffect`] component for any newly allocated effect.
2990
///
2991
/// After this system ran, and its commands are applied, all valid extracted
2992
/// effects have a corresponding entity in the render world, with a
2993
/// [`CachedEffect`] component. From there, we operate on those exclusively.
2994
pub(crate) fn add_effects(
×
2995
    commands: Commands,
2996
    mut effects_meta: ResMut<EffectsMeta>,
2997
    mut effect_cache: ResMut<EffectCache>,
2998
    mut property_cache: ResMut<PropertyCache>,
2999
    mut event_cache: ResMut<EventCache>,
3000
    mut extracted_effects: ResMut<ExtractedEffects>,
3001
    mut sort_bind_groups: ResMut<SortBindGroups>,
3002
) {
3003
    #[cfg(feature = "trace")]
NEW
3004
    let _span = bevy::log::info_span!("add_effects").entered();
×
3005
    trace!("add_effects");
×
3006

3007
    // Clear last frame's buffer resizes which may have occured during last frame,
3008
    // during `Node::run()` while the `BufferTable` could not be mutated. This is
3009
    // the first point at which we can do that where we're not blocking the main
3010
    // world (so, excluding the extract system).
3011
    effects_meta
×
3012
        .update_dispatch_indirect_buffer
×
3013
        .clear_previous_frame_resizes();
3014
    effects_meta
×
3015
        .effect_metadata_buffer
×
3016
        .clear_previous_frame_resizes();
3017
    sort_bind_groups.clear_previous_frame_resizes();
×
3018
    event_cache.clear_previous_frame_resizes();
×
3019

3020
    // Allocate new effects
3021
    effects_meta.add_effects(
×
3022
        commands,
×
3023
        std::mem::take(&mut extracted_effects.added_effects),
×
3024
        &mut effect_cache,
×
3025
        &mut property_cache,
×
3026
        &mut event_cache,
×
3027
    );
3028

3029
    // Note: we don't need to explicitly allocate GPU buffers for effects,
3030
    // because EffectBuffer already contains a reference to the
3031
    // RenderDevice, so has done so internally. This is not ideal
3032
    // design-wise, but works.
3033
}
3034

3035
/// Check if two lists of entities are equal.
3036
fn is_child_list_changed(
×
3037
    parent_entity: Entity,
3038
    old: impl ExactSizeIterator<Item = Entity>,
3039
    new: impl ExactSizeIterator<Item = Entity>,
3040
) -> bool {
3041
    if old.len() != new.len() {
×
3042
        trace!(
×
3043
            "Child list changed for effect {:?}: old #{} != new #{}",
×
3044
            parent_entity,
×
3045
            old.len(),
×
3046
            new.len()
×
3047
        );
3048
        return true;
×
3049
    }
3050

3051
    // TODO - this value is arbitrary
3052
    if old.len() >= 16 {
×
3053
        // For large-ish lists, use a hash set.
NEW
3054
        let old = HashSet::<Entity, bevy::platform::hash::FixedHasher>::from_iter(old);
×
NEW
3055
        let new = HashSet::<Entity, bevy::platform::hash::FixedHasher>::from_iter(new);
×
3056
        if old != new {
×
3057
            trace!(
×
3058
                "Child list changed for effect {parent_entity:?}: old [{old:?}] != new [{new:?}]"
×
3059
            );
3060
            true
×
3061
        } else {
3062
            false
×
3063
        }
3064
    } else {
3065
        // For small lists, just use a linear array and sort it
3066
        let mut old = old.collect::<Vec<_>>();
×
3067
        let mut new = new.collect::<Vec<_>>();
×
3068
        old.sort_unstable();
×
3069
        new.sort_unstable();
×
3070
        if old != new {
×
3071
            trace!(
×
3072
                "Child list changed for effect {parent_entity:?}: old [{old:?}] != new [{new:?}]"
×
3073
            );
3074
            true
×
3075
        } else {
3076
            false
×
3077
        }
3078
    }
3079
}
3080

3081
/// Resolve parents and children, updating their [`CachedParent`] and
3082
/// [`CachedChild`] components, as well as (re-)allocating any [`GpuChildInfo`]
3083
/// slice for all children of each parent.
3084
pub(crate) fn resolve_parents(
×
3085
    mut commands: Commands,
3086
    q_child_effects: Query<
3087
        (
3088
            Entity,
3089
            &CachedParentRef,
3090
            &CachedEffectEvents,
3091
            Option<&CachedChildInfo>,
3092
        ),
3093
        With<CachedEffect>,
3094
    >,
3095
    q_cached_effects: Query<(Entity, MainEntity, &CachedEffect)>,
3096
    effect_cache: Res<EffectCache>,
3097
    mut q_parent_effects: Query<(Entity, &mut CachedParentInfo), With<CachedEffect>>,
3098
    mut event_cache: ResMut<EventCache>,
3099
    mut children_from_parent: Local<
3100
        HashMap<Entity, (Vec<(Entity, BufferBindingSource)>, Vec<GpuChildInfo>)>,
3101
    >,
3102
) {
3103
    #[cfg(feature = "trace")]
NEW
3104
    let _span = bevy::log::info_span!("resolve_parents").entered();
×
3105
    let num_parent_effects = q_parent_effects.iter().len();
×
3106
    trace!("resolve_parents: num_parents={num_parent_effects}");
×
3107

3108
    // Build map of render entity from main entity for all cached effects.
3109
    let render_from_main_entity = q_cached_effects
×
3110
        .iter()
3111
        .map(|(render_entity, main_entity, _)| (main_entity, render_entity))
×
3112
        .collect::<HashMap<_, _>>();
3113

3114
    // Record all parents with children that changed so that we can mark those
3115
    // parents' `CachedParentInfo` as changed. See the comment in the
3116
    // `q_parent_effects` loop for more information.
3117
    let mut parents_with_dirty_children = EntityHashSet::default();
×
3118

3119
    // Group child effects by parent, building a list of children for each parent,
3120
    // solely based on the declaration each child makes of its parent. This doesn't
3121
    // mean yet that the parent exists.
3122
    if children_from_parent.capacity() < num_parent_effects {
×
3123
        let extra = num_parent_effects - children_from_parent.capacity();
×
3124
        children_from_parent.reserve(extra);
×
3125
    }
3126
    for (child_entity, cached_parent_ref, cached_effect_events, cached_child_info) in
×
3127
        q_child_effects.iter()
×
3128
    {
3129
        // Resolve the parent reference into the render world
3130
        let parent_main_entity = cached_parent_ref.entity;
3131
        let Some(parent_entity) = render_from_main_entity.get(&parent_main_entity.id()) else {
×
3132
            warn!(
×
3133
                "Cannot resolve parent render entity for parent main entity {:?}, removing CachedChildInfo from child entity {:?}.",
×
3134
                parent_main_entity, child_entity
3135
            );
3136
            commands.entity(child_entity).remove::<CachedChildInfo>();
×
3137
            continue;
×
3138
        };
3139
        let parent_entity = *parent_entity;
3140

3141
        // Resolve the parent
3142
        let Ok((_, _, parent_cached_effect)) = q_cached_effects.get(parent_entity) else {
×
3143
            // Since we failed to resolve, remove this component so the next systems ignore
3144
            // this effect.
3145
            warn!(
×
3146
                "Unknown parent render entity {:?}, removing CachedChildInfo from child entity {:?}.",
×
3147
                parent_entity, child_entity
3148
            );
3149
            commands.entity(child_entity).remove::<CachedChildInfo>();
×
3150
            continue;
×
3151
        };
3152
        let Some(parent_buffer_binding_source) = effect_cache
×
3153
            .get_buffer(parent_cached_effect.buffer_index)
3154
            .map(|effect_buffer| effect_buffer.max_binding_source())
×
3155
        else {
3156
            // Since we failed to resolve, remove this component so the next systems ignore
3157
            // this effect.
3158
            warn!(
×
3159
                "Unknown parent buffer #{} on entity {:?}, removing CachedChildInfo.",
×
3160
                parent_cached_effect.buffer_index, child_entity
3161
            );
3162
            commands.entity(child_entity).remove::<CachedChildInfo>();
×
3163
            continue;
×
3164
        };
3165

3166
        let Some(child_event_buffer) = event_cache.get_buffer(cached_effect_events.buffer_index)
×
3167
        else {
3168
            // Since we failed to resolve, remove this component so the next systems ignore
3169
            // this effect.
3170
            warn!(
×
3171
                "Unknown child event buffer #{} on entity {:?}, removing CachedChildInfo.",
×
3172
                cached_effect_events.buffer_index, child_entity
3173
            );
3174
            commands.entity(child_entity).remove::<CachedChildInfo>();
×
3175
            continue;
×
3176
        };
3177
        let child_buffer_binding_source = BufferBindingSource {
3178
            buffer: child_event_buffer.clone(),
3179
            offset: cached_effect_events.range.start,
3180
            size: NonZeroU32::new(cached_effect_events.range.len() as u32).unwrap(),
3181
        };
3182

3183
        // Push the child entity into the children list
3184
        let (child_vec, child_infos) = children_from_parent.entry(parent_entity).or_default();
3185
        let local_child_index = child_vec.len() as u32;
3186
        child_vec.push((child_entity, child_buffer_binding_source));
3187
        child_infos.push(GpuChildInfo {
3188
            event_count: 0,
3189
            init_indirect_dispatch_index: cached_effect_events.init_indirect_dispatch_index,
3190
        });
3191

3192
        // Check if child info changed. Avoid overwriting if no change.
3193
        if let Some(old_cached_child_info) = cached_child_info {
×
3194
            if parent_entity == old_cached_child_info.parent
3195
                && parent_cached_effect.slice.particle_layout
×
3196
                    == old_cached_child_info.parent_particle_layout
×
3197
                && parent_buffer_binding_source
×
3198
                    == old_cached_child_info.parent_buffer_binding_source
×
3199
                // 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.
3200
                && local_child_index == old_cached_child_info.local_child_index
×
3201
                && cached_effect_events.init_indirect_dispatch_index
×
3202
                    == old_cached_child_info.init_indirect_dispatch_index
×
3203
            {
3204
                trace!(
×
3205
                    "ChildInfo didn't change for child entity {:?}, skipping component write.",
×
3206
                    child_entity
3207
                );
3208
                continue;
×
3209
            }
3210
        }
3211

3212
        // Allocate (or overwrite, if already existing) the child info, now that the
3213
        // parent is resolved.
3214
        let cached_child_info = CachedChildInfo {
3215
            parent: parent_entity,
3216
            parent_particle_layout: parent_cached_effect.slice.particle_layout.clone(),
×
3217
            parent_buffer_binding_source,
3218
            local_child_index,
3219
            global_child_index: u32::MAX, // fixed up later by fixup_parents()
3220
            init_indirect_dispatch_index: cached_effect_events.init_indirect_dispatch_index,
×
3221
        };
3222
        commands.entity(child_entity).insert(cached_child_info);
×
3223
        trace!("Spawned CachedChildInfo on child entity {:?}", child_entity);
×
3224

3225
        // Make a note of the parent entity so that we remember to mark its
3226
        // `CachedParentInfo` as changed below.
3227
        parents_with_dirty_children.insert(parent_entity);
3228
    }
3229

3230
    // Once all parents are resolved, diff all children of already-cached parents,
3231
    // and re-allocate their GpuChildInfo if needed.
3232
    for (parent_entity, mut cached_parent_info) in q_parent_effects.iter_mut() {
×
3233
        // Fetch the newly extracted list of children
3234
        let Some((_, (children, child_infos))) = children_from_parent.remove_entry(&parent_entity)
×
3235
        else {
3236
            trace!("Entity {parent_entity:?} is no more a parent, removing CachedParentInfo component...");
×
3237
            commands.entity(parent_entity).remove::<CachedParentInfo>();
×
3238
            continue;
×
3239
        };
3240

3241
        // If we updated `CachedChildInfo` for any of this entity's children,
3242
        // then even if the check below passes, we must still set the change
3243
        // flag on this entity's `CachedParentInfo`. That's because the
3244
        // `fixup_parents` system looks at the change flag for the parent in
3245
        // order to determine which `CachedChildInfo` it needs to update, and
3246
        // that system must process all newly-added `CachedChildInfo`s.
3247
        if parents_with_dirty_children.contains(&parent_entity) {
×
3248
            cached_parent_info.set_changed();
×
3249
        }
3250

3251
        // Check if any child changed compared to the existing CachedChildren component
3252
        if !is_child_list_changed(
3253
            parent_entity,
3254
            cached_parent_info
3255
                .children
3256
                .iter()
3257
                .map(|(entity, _)| *entity),
×
3258
            children.iter().map(|(entity, _)| *entity),
×
3259
        ) {
3260
            continue;
×
3261
        }
3262

3263
        event_cache.reallocate_child_infos(
×
3264
            parent_entity,
×
3265
            children,
×
3266
            &child_infos[..],
×
3267
            cached_parent_info.deref_mut(),
×
3268
        );
3269
    }
3270

3271
    // Once this is done, the children hash map contains all entries which don't
3272
    // already have a CachedParentInfo component. That is, all entities which are
3273
    // new parents.
3274
    for (parent_entity, (children, child_infos)) in children_from_parent.drain() {
×
3275
        let cached_parent_info =
3276
            event_cache.allocate_child_infos(parent_entity, children, &child_infos[..]);
3277
        commands.entity(parent_entity).insert(cached_parent_info);
3278
    }
3279

3280
    // // Once all changes are applied, immediately schedule any GPU buffer
3281
    // // (re)allocation based on the new buffer size. The actual GPU buffer
3282
    // content // will be written later.
3283
    // if event_cache
3284
    //     .child_infos()
3285
    //     .allocate_gpu(render_device, render_queue)
3286
    // {
3287
    //     // All those bind groups use the buffer so need to be re-created
3288
    //     effect_bind_groups.particle_buffers.clear();
3289
    // }
3290
}
3291

3292
pub fn fixup_parents(
×
3293
    q_changed_parents: Query<(Entity, &CachedParentInfo), Changed<CachedParentInfo>>,
3294
    mut q_children: Query<&mut CachedChildInfo>,
3295
) {
3296
    #[cfg(feature = "trace")]
NEW
3297
    let _span = bevy::log::info_span!("fixup_parents").entered();
×
3298
    trace!("fixup_parents");
×
3299

3300
    // Once all parents are (re-)allocated, fix up the global index of all
3301
    // children if the parent base index changed.
3302
    trace!(
×
3303
        "Updating the global index of children of parent effects whose child list just changed..."
×
3304
    );
3305
    for (parent_entity, cached_parent_info) in q_changed_parents.iter() {
×
3306
        let base_index =
3307
            cached_parent_info.byte_range.start / GpuChildInfo::SHADER_SIZE.get() as u32;
3308
        trace!(
3309
            "Updating {} children of parent effect {:?} with base child index {}...",
×
3310
            cached_parent_info.children.len(),
×
3311
            parent_entity,
3312
            base_index
3313
        );
3314
        for (child_entity, _) in &cached_parent_info.children {
×
3315
            let Ok(mut cached_child_info) = q_children.get_mut(*child_entity) else {
×
3316
                continue;
×
3317
            };
3318
            cached_child_info.global_child_index = base_index + cached_child_info.local_child_index;
×
3319
            trace!(
×
3320
                "+ Updated global index for child ID {:?} of parent {:?}: local={}, global={}",
×
3321
                child_entity,
×
3322
                parent_entity,
×
3323
                cached_child_info.local_child_index,
×
3324
                cached_child_info.global_child_index
×
3325
            );
3326
        }
3327
    }
3328
}
3329

3330
/// Update any cached mesh info based on any relocation done by Bevy itself.
3331
pub fn update_mesh_locations(
×
3332
    mut commands: Commands,
3333
    mesh_allocator: Res<MeshAllocator>,
3334
    render_meshes: Res<RenderAssets<RenderMesh>>,
3335
    mut q_cached_effects: Query<
3336
        (Entity, &CachedMesh, Option<&mut CachedMeshLocation>),
3337
        With<CachedEffect>,
3338
    >,
3339
) {
3340
    for (entity, cached_mesh, maybe_cached_mesh_location) in &mut q_cached_effects {
×
3341
        // Resolve the render mesh
3342
        let Some(render_mesh) = render_meshes.get(cached_mesh.mesh) else {
×
3343
            warn!(
×
3344
                "Cannot find render mesh of particle effect instance on entity {:?}, despite applying default mesh. Invalid asset handle: {:?}",
×
3345
                entity, cached_mesh.mesh
3346
            );
3347
            continue;
×
3348
        };
3349

3350
        // Find the location where the render mesh was allocated. This is handled by
3351
        // Bevy itself in the allocate_and_free_meshes() system. Bevy might
3352
        // re-batch the vertex and optional index data of meshes together at any point,
3353
        // so we need to confirm that the location data we may have cached is still
3354
        // valid.
3355
        let Some(mesh_vertex_buffer_slice) = mesh_allocator.mesh_vertex_slice(&cached_mesh.mesh)
×
3356
        else {
3357
            trace!(
×
3358
                "Effect main_entity {:?}: cannot find vertex slice of render mesh {:?}",
×
3359
                entity,
3360
                cached_mesh.mesh
3361
            );
3362
            continue;
×
3363
        };
3364
        let mesh_index_buffer_slice = mesh_allocator.mesh_index_slice(&cached_mesh.mesh);
3365
        let indexed =
×
3366
            if let RenderMeshBufferInfo::Indexed { index_format, .. } = render_mesh.buffer_info {
×
3367
                if let Some(ref slice) = mesh_index_buffer_slice {
×
3368
                    Some(MeshIndexSlice {
3369
                        format: index_format,
3370
                        buffer: slice.buffer.clone(),
3371
                        range: slice.range.clone(),
3372
                    })
3373
                } else {
3374
                    trace!(
×
3375
                        "Effect main_entity {:?}: cannot find index slice of render mesh {:?}",
×
3376
                        entity,
3377
                        cached_mesh.mesh
3378
                    );
3379
                    continue;
×
3380
                }
3381
            } else {
3382
                None
×
3383
            };
3384

3385
        // Calculate the new mesh location as it should be based on Bevy's info
3386
        let new_mesh_location = match &mesh_index_buffer_slice {
3387
            // Indexed mesh rendering
3388
            Some(mesh_index_buffer_slice) => CachedMeshLocation {
3389
                vertex_buffer: mesh_vertex_buffer_slice.buffer.id(),
×
3390
                vertex_or_index_count: mesh_index_buffer_slice.range.len() as u32,
×
3391
                first_index_or_vertex_offset: mesh_index_buffer_slice.range.start,
×
3392
                vertex_offset_or_base_instance: mesh_vertex_buffer_slice.range.start as i32,
×
3393
                indexed,
3394
            },
3395
            // Non-indexed mesh rendering
3396
            None => CachedMeshLocation {
3397
                vertex_buffer: mesh_vertex_buffer_slice.buffer.id(),
×
3398
                vertex_or_index_count: mesh_vertex_buffer_slice.range.len() as u32,
×
3399
                first_index_or_vertex_offset: mesh_vertex_buffer_slice.range.start,
×
3400
                vertex_offset_or_base_instance: 0,
3401
                indexed: None,
3402
            },
3403
        };
3404

3405
        // Compare to any cached data and update if necessary, or insert if missing.
3406
        // This will trigger change detection in the ECS, which will in turn trigger
3407
        // GpuEffectMetadata re-upload.
3408
        if let Some(mut old_mesh_location) = maybe_cached_mesh_location {
×
3409
            #[cfg(debug_assertions)]
3410
            if *old_mesh_location.deref() != new_mesh_location {
3411
                debug!(
×
3412
                    "Mesh location changed for asset {:?}\nold:{:?}\nnew:{:?}",
×
3413
                    entity, old_mesh_location, new_mesh_location
3414
                );
3415
            }
3416

3417
            old_mesh_location.set_if_neq(new_mesh_location);
3418
        } else {
3419
            commands.entity(entity).insert(new_mesh_location);
×
3420
        }
3421
    }
3422
}
3423

3424
// TEMP - Mark all cached effects as invalid for this frame until another system
3425
// explicitly marks them as valid. Otherwise we early out in some parts, and
3426
// reuse by mistake the previous frame's extraction.
3427
pub fn clear_transient_batch_inputs(
×
3428
    mut commands: Commands,
3429
    mut q_cached_effects: Query<Entity, With<BatchInput>>,
3430
) {
3431
    for entity in &mut q_cached_effects {
×
NEW
3432
        if let Ok(mut cmd) = commands.get_entity(entity) {
×
3433
            cmd.remove::<BatchInput>();
3434
        }
3435
    }
3436
}
3437

3438
/// Render world cached mesh infos for a single effect instance.
3439
#[derive(Debug, Clone, Copy, Component)]
3440
pub(crate) struct CachedMesh {
3441
    /// Asset of the effect mesh to draw.
3442
    pub mesh: AssetId<Mesh>,
3443
}
3444

3445
/// Indexed mesh metadata for [`CachedMesh`].
3446
#[derive(Debug, Clone)]
3447
#[allow(dead_code)]
3448
pub(crate) struct MeshIndexSlice {
3449
    /// Index format.
3450
    pub format: IndexFormat,
3451
    /// GPU buffer containing the indices.
3452
    pub buffer: Buffer,
3453
    /// Range inside [`Self::buffer`] where the indices are.
3454
    pub range: Range<u32>,
3455
}
3456

3457
impl PartialEq for MeshIndexSlice {
3458
    fn eq(&self, other: &Self) -> bool {
×
3459
        self.format == other.format
×
3460
            && self.buffer.id() == other.buffer.id()
×
3461
            && self.range == other.range
×
3462
    }
3463
}
3464

3465
impl Eq for MeshIndexSlice {}
3466

3467
/// Cached info about a mesh location in a Bevy buffer. This information is
3468
/// uploaded to GPU into [`GpuEffectMetadata`] for indirect rendering, but is
3469
/// also kept CPU side in this component to detect when Bevy relocated a mesh,
3470
/// so we can invalidate that GPU data.
3471
#[derive(Debug, Clone, PartialEq, Eq, Component)]
3472
pub(crate) struct CachedMeshLocation {
3473
    /// Vertex buffer.
3474
    pub vertex_buffer: BufferId,
3475
    /// See [`GpuEffectMetadata::vertex_or_index_count`].
3476
    pub vertex_or_index_count: u32,
3477
    /// See [`GpuEffectMetadata::first_index_or_vertex_offset`].
3478
    pub first_index_or_vertex_offset: u32,
3479
    /// See [`GpuEffectMetadata::vertex_offset_or_base_instance`].
3480
    pub vertex_offset_or_base_instance: i32,
3481
    /// Indexed rendering metadata.
3482
    pub indexed: Option<MeshIndexSlice>,
3483
}
3484

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

3499
#[derive(SystemParam)]
3500
pub struct PrepareEffectsReadOnlyParams<'w, 's> {
3501
    sim_params: Res<'w, SimParams>,
3502
    render_device: Res<'w, RenderDevice>,
3503
    render_queue: Res<'w, RenderQueue>,
3504
    marker: PhantomData<&'s usize>,
3505
}
3506

3507
#[derive(SystemParam)]
3508
pub struct PipelineSystemParams<'w, 's> {
3509
    pipeline_cache: Res<'w, PipelineCache>,
3510
    init_pipeline: ResMut<'w, ParticlesInitPipeline>,
3511
    indirect_pipeline: Res<'w, DispatchIndirectPipeline>,
3512
    update_pipeline: ResMut<'w, ParticlesUpdatePipeline>,
3513
    specialized_init_pipelines: ResMut<'w, SpecializedComputePipelines<ParticlesInitPipeline>>,
3514
    specialized_update_pipelines: ResMut<'w, SpecializedComputePipelines<ParticlesUpdatePipeline>>,
3515
    specialized_indirect_pipelines:
3516
        ResMut<'w, SpecializedComputePipelines<DispatchIndirectPipeline>>,
3517
    marker: PhantomData<&'s usize>,
3518
}
3519

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

3551
    init_fill_dispatch_queue.clear();
×
3552

3553
    // Workaround for too many params in system (TODO: refactor to split work?)
3554
    let sim_params = read_only_params.sim_params.into_inner();
×
3555
    let render_device = read_only_params.render_device.into_inner();
×
3556
    let render_queue = read_only_params.render_queue.into_inner();
×
3557
    let pipeline_cache = pipelines.pipeline_cache.into_inner();
×
3558
    let specialized_init_pipelines = pipelines.specialized_init_pipelines.into_inner();
×
3559
    let specialized_update_pipelines = pipelines.specialized_update_pipelines.into_inner();
×
3560
    let specialized_indirect_pipelines = pipelines.specialized_indirect_pipelines.into_inner();
×
3561

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

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

3605
    gpu_buffer_operations.begin_frame();
×
3606

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

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

3651
        let effect_slice = EffectSlice {
3652
            slice: cached_effect.slice.range(),
3653
            buffer_index: cached_effect.buffer_index,
3654
            particle_layout: cached_effect.slice.particle_layout.clone(),
3655
        };
3656

3657
        let has_event_buffer = cached_child_info.is_some();
3658
        // FIXME: decouple "consumes event" from "reads parent particle" (here, p.layout
3659
        // should be Option<T>, not T)
3660
        let property_layout_min_binding_size = if extracted_effect.property_layout.is_empty() {
3661
            None
×
3662
        } else {
3663
            Some(extracted_effect.property_layout.min_binding_size())
×
3664
        };
3665

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

3678
            // Resolve parent entry
3679
            let Ok((_, _, _, _, _, _, cached_parent_info, _, _)) =
×
3680
                q_cached_effects.get(cached_child_info.parent)
×
3681
            else {
3682
                continue;
×
3683
            };
3684
            let Some(cached_parent_info) = cached_parent_info else {
×
3685
                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);
×
3686
                continue;
×
3687
            };
3688

3689
            let init_indirect_dispatch_index = cached_effect_events.init_indirect_dispatch_index;
3690
            assert_eq!(0, cached_parent_info.byte_range.start % 4);
3691
            let global_child_index = cached_child_info.global_child_index;
×
3692

3693
            // Schedule a fill dispatch
3694
            trace!(
×
3695
                "init_fill_dispatch.push(): src:global_child_index={} dst:init_indirect_dispatch_index={}",
×
3696
                global_child_index,
3697
                init_indirect_dispatch_index,
3698
            );
3699
            init_fill_dispatch_queue.enqueue(global_child_index, init_indirect_dispatch_index);
×
3700
        }
3701

3702
        // Create init pipeline key flags.
3703
        let init_pipeline_key_flags = {
×
3704
            let mut flags = ParticleInitPipelineKeyFlags::empty();
×
3705
            flags.set(
×
3706
                ParticleInitPipelineKeyFlags::ATTRIBUTE_PREV,
×
3707
                effect_slice.particle_layout.contains(Attribute::PREV),
×
3708
            );
3709
            flags.set(
×
3710
                ParticleInitPipelineKeyFlags::ATTRIBUTE_NEXT,
×
3711
                effect_slice.particle_layout.contains(Attribute::NEXT),
×
3712
            );
3713
            flags.set(
×
3714
                ParticleInitPipelineKeyFlags::CONSUME_GPU_SPAWN_EVENTS,
×
3715
                has_event_buffer,
×
3716
            );
3717
            flags
×
3718
        };
3719

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

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

3780
        let particle_layout_min_binding_size = effect_slice.particle_layout.min_binding_size32();
3781
        let spawner_bind_group_layout = spawner_bind_group_layout.clone();
3782

3783
        // Specialize the init pipeline based on the effect.
3784
        let init_pipeline_id = {
3785
            let consume_gpu_spawn_events = init_pipeline_key_flags
3786
                .contains(ParticleInitPipelineKeyFlags::CONSUME_GPU_SPAWN_EVENTS);
3787

3788
            // Fetch the metadata@3 bind group layout from the cache
3789
            let metadata_bind_group_layout = effect_cache
3790
                .metadata_init_bind_group_layout(consume_gpu_spawn_events)
3791
                .unwrap()
3792
                .clone();
3793

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

3823
            init_pipeline_id
3824
        };
3825

3826
        let update_pipeline_id = {
3827
            let num_event_buffers = cached_parent_info
3828
                .map(|p| p.children.len() as u32)
×
3829
                .unwrap_or_default();
3830

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

3838
            // Fetch the bind group layouts from the cache
3839
            let metadata_bind_group_layout = effect_cache
3840
                .metadata_update_bind_group_layout(num_event_buffers)
3841
                .unwrap()
3842
                .clone();
3843

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

3873
            update_pipeline_id
3874
        };
3875

3876
        let init_and_update_pipeline_ids = InitAndUpdatePipelineIds {
3877
            init: init_pipeline_id,
3878
            update: update_pipeline_id,
3879
        };
3880

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

3896
        // Output some debug info
3897
        trace!("init_shader = {:?}", extracted_effect.effect_shaders.init);
×
3898
        trace!(
3899
            "update_shader = {:?}",
×
3900
            extracted_effect.effect_shaders.update
3901
        );
3902
        trace!(
3903
            "render_shader = {:?}",
×
3904
            extracted_effect.effect_shaders.render
3905
        );
3906
        trace!("layout_flags = {:?}", extracted_effect.layout_flags);
×
3907
        trace!("particle_layout = {:?}", effect_slice.particle_layout);
×
3908

3909
        let spawner_index = effects_meta.allocate_spawner(
3910
            &extracted_effect.transform,
3911
            extracted_effect.spawn_count,
3912
            extracted_effect.prng_seed,
3913
            dispatch_buffer_indices.effect_metadata_buffer_table_id,
3914
        );
3915

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

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

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

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

4009
        // Now that the effect is entirely prepared and all GPU resources are allocated,
4010
        // update its GpuEffectMetadata with all those infos.
4011
        // FIXME - should do this only when the below changes (not only the mesh), via
4012
        // some invalidation mechanism and ECS change detection.
4013
        if !cached_mesh.is_changed() && !cached_mesh_location.is_changed() {
×
4014
            prepared_effect_count += 1;
×
4015
            continue;
×
4016
        }
4017

4018
        let capacity = cached_effect.slice.len();
×
4019

4020
        // Global and local indices of this effect as a child of another (parent) effect
4021
        let (global_child_index, local_child_index) = cached_child_info
×
4022
            .map(|cci| (cci.global_child_index, cci.local_child_index))
×
4023
            .unwrap_or_default();
4024

4025
        // Base index of all children of this (parent) effect
4026
        let base_child_index = cached_parent_info
4027
            .map(|cpi| {
×
4028
                debug_assert_eq!(
×
4029
                    cpi.byte_range.start % GpuChildInfo::SHADER_SIZE.get() as u32,
×
4030
                    0
4031
                );
4032
                cpi.byte_range.start / GpuChildInfo::SHADER_SIZE.get() as u32
×
4033
            })
4034
            .unwrap_or_default();
4035

4036
        let particle_stride = extracted_effect.particle_layout.min_binding_size32().get() / 4;
4037
        let sort_key_offset = extracted_effect
4038
            .particle_layout
4039
            .offset(Attribute::RIBBON_ID)
4040
            .unwrap_or_default()
4041
            / 4;
4042
        let sort_key2_offset = extracted_effect
4043
            .particle_layout
4044
            .offset(Attribute::AGE)
4045
            .unwrap_or_default()
4046
            / 4;
4047

4048
        let gpu_effect_metadata = GpuEffectMetadata {
4049
            vertex_or_index_count: cached_mesh_location.vertex_or_index_count,
4050
            instance_count: 0,
4051
            first_index_or_vertex_offset: cached_mesh_location.first_index_or_vertex_offset,
4052
            vertex_offset_or_base_instance: cached_mesh_location.vertex_offset_or_base_instance,
4053
            base_instance: 0,
4054
            alive_count: 0,
4055
            max_update: 0,
4056
            dead_count: capacity,
4057
            max_spawn: capacity,
4058
            ping: 0,
4059
            indirect_dispatch_index: dispatch_buffer_indices
4060
                .update_dispatch_indirect_buffer_row_index,
4061
            // Note: the indirect draw args are at the start of the GpuEffectMetadata struct
4062
            indirect_render_index: dispatch_buffer_indices.effect_metadata_buffer_table_id.0,
4063
            init_indirect_dispatch_index: cached_effect_events
4064
                .map(|cee| cee.init_indirect_dispatch_index)
4065
                .unwrap_or_default(),
4066
            local_child_index,
4067
            global_child_index,
4068
            base_child_index,
4069
            particle_stride,
4070
            sort_key_offset,
4071
            sort_key2_offset,
4072
            ..default()
4073
        };
4074

4075
        assert!(dispatch_buffer_indices
4076
            .effect_metadata_buffer_table_id
4077
            .is_valid());
4078
        effects_meta.effect_metadata_buffer.update(
×
4079
            dispatch_buffer_indices.effect_metadata_buffer_table_id,
×
4080
            gpu_effect_metadata,
×
4081
        );
4082

4083
        warn!(
×
4084
            "Updated metadata entry {} for effect {:?}, this will reset it.",
×
4085
            dispatch_buffer_indices.effect_metadata_buffer_table_id.0, main_entity
4086
        );
4087

4088
        prepared_effect_count += 1;
4089
    }
4090
    trace!("Prepared {prepared_effect_count}/{extracted_effect_count} extracted effect(s)");
×
4091

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

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

4119
    // Update simulation parameters
4120
    effects_meta.sim_params_uniforms.set(sim_params.into());
×
4121
    {
4122
        let gpu_sim_params = effects_meta.sim_params_uniforms.get_mut();
×
4123
        gpu_sim_params.num_effects = prepared_effect_count;
×
4124

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

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

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

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

4178
    let mut sort_queue = GpuBufferOperationQueue::new();
×
4179

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

4212
        let translation = input.position;
×
4213

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

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

4241
            // Allocate a GpuDispatchIndirect entry
4242
            let sort_fill_indirect_dispatch_index = sort_bind_groups.allocate_indirect_dispatch();
4243
            effect_batch.sort_fill_indirect_dispatch_index =
4244
                Some(sort_fill_indirect_dispatch_index);
4245

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

4310
        let effect_batch_index = sorted_effect_batches.push(effect_batch);
×
4311
        trace!(
×
4312
            "Spawned effect batch #{:?} from cached instance on entity {:?}.",
×
4313
            effect_batch_index,
4314
            entity,
4315
        );
4316

4317
        // Spawn an EffectDrawBatch, to actually drive rendering.
4318
        commands
4319
            .spawn(EffectDrawBatch {
4320
                effect_batch_index,
4321
                translation,
4322
                main_entity: *main_entity,
4323
            })
4324
            .insert(TemporaryRenderEntity);
4325
    }
4326

4327
    debug_assert!(sorted_effect_batches.dispatch_queue_index.is_none());
×
4328
    if !sort_queue.operation_queue.is_empty() {
×
4329
        sorted_effect_batches.dispatch_queue_index = Some(gpu_buffer_operations.submit(sort_queue));
×
4330
    }
4331

4332
    sorted_effect_batches.sort();
×
4333
}
4334

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

4372
/// Combination of a texture layout and the bound textures.
4373
#[derive(Debug, Default, Clone, PartialEq, Eq, Hash)]
4374
struct Material {
4375
    layout: TextureLayout,
4376
    textures: Vec<AssetId<Image>>,
4377
}
4378

4379
impl Material {
4380
    /// Get the bind group entries to create a bind group.
4381
    pub fn make_entries<'a>(
×
4382
        &self,
4383
        gpu_images: &'a RenderAssets<GpuImage>,
4384
    ) -> Result<Vec<BindGroupEntry<'a>>, ()> {
4385
        if self.textures.is_empty() {
×
4386
            return Ok(vec![]);
×
4387
        }
4388

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

4418
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
4419
struct BindingKey {
4420
    pub buffer_id: BufferId,
4421
    pub offset: u32,
4422
    pub size: NonZeroU32,
4423
}
4424

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

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

4445
impl From<&BufferBindingSource> for BindingKey {
4446
    fn from(value: &BufferBindingSource) -> Self {
×
4447
        Self {
4448
            buffer_id: value.buffer.id(),
×
4449
            offset: value.offset,
×
4450
            size: value.size,
×
4451
        }
4452
    }
4453
}
4454

4455
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
4456
struct ConsumeEventKey {
4457
    child_infos_buffer_id: BufferId,
4458
    events: BindingKey,
4459
}
4460

4461
impl From<&ConsumeEventBuffers<'_>> for ConsumeEventKey {
4462
    fn from(value: &ConsumeEventBuffers) -> Self {
×
4463
        Self {
4464
            child_infos_buffer_id: value.child_infos_buffer.id(),
×
4465
            events: value.events.into(),
×
4466
        }
4467
    }
4468
}
4469

4470
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
4471
struct InitMetadataBindGroupKey {
4472
    pub buffer_index: u32,
4473
    pub effect_metadata_buffer: BufferId,
4474
    pub effect_metadata_offset: u32,
4475
    pub consume_event_key: Option<ConsumeEventKey>,
4476
}
4477

4478
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
4479
struct UpdateMetadataBindGroupKey {
4480
    pub buffer_index: u32,
4481
    pub effect_metadata_buffer: BufferId,
4482
    pub effect_metadata_offset: u32,
4483
    pub child_info_buffer_id: Option<BufferId>,
4484
    pub event_buffers_keys: Vec<BindingKey>,
4485
}
4486

4487
struct CachedBindGroup<K: Eq> {
4488
    /// Key the bind group was created from. Each time the key changes, the bind
4489
    /// group should be re-created.
4490
    key: K,
4491
    /// Bind group created from the key.
4492
    bind_group: BindGroup,
4493
}
4494

4495
#[derive(Debug, Clone, Copy)]
4496
struct BufferSlice<'a> {
4497
    pub buffer: &'a Buffer,
4498
    pub offset: u32,
4499
    pub size: NonZeroU32,
4500
}
4501

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

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

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

4532
/// Optional input to [`EffectBindGroups::get_or_create_init_metadata()`] when
4533
/// the init pass consumes GPU events as a mechanism to spawn particles.
4534
struct ConsumeEventBuffers<'a> {
4535
    /// Entire buffer containing the [`GpuChildInfo`] entries for all effects.
4536
    /// This is dynamically indexed inside the shader.
4537
    child_infos_buffer: &'a Buffer,
4538
    /// Slice of the [`EventBuffer`] where the GPU spawn events are stored.
4539
    events: BufferSlice<'a>,
4540
}
4541

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

4561
impl EffectBindGroups {
4562
    pub fn particle_render(&self, buffer_index: u32) -> Option<&BindGroup> {
×
4563
        self.particle_buffers
×
4564
            .get(&buffer_index)
×
4565
            .map(|bg| &bg.render)
×
4566
    }
4567

4568
    /// Retrieve the metadata@3 bind group for the init pass, creating it if
4569
    /// needed.
4570
    pub(self) fn get_or_create_init_metadata(
×
4571
        &mut self,
4572
        effect_batch: &EffectBatch,
4573
        gpu_limits: &GpuLimits,
4574
        render_device: &RenderDevice,
4575
        layout: &BindGroupLayout,
4576
        effect_metadata_buffer: &Buffer,
4577
        consume_event_buffers: Option<ConsumeEventBuffers>,
4578
    ) -> Result<&BindGroup, ()> {
4579
        let DispatchBufferIndices {
×
4580
            effect_metadata_buffer_table_id,
×
4581
            ..
×
4582
        } = &effect_batch.dispatch_buffer_indices;
×
4583

4584
        let effect_metadata_offset =
×
4585
            gpu_limits.effect_metadata_offset(effect_metadata_buffer_table_id.0) as u32;
×
4586
        let key = InitMetadataBindGroupKey {
4587
            buffer_index: effect_batch.buffer_index,
×
4588
            effect_metadata_buffer: effect_metadata_buffer.id(),
×
4589
            effect_metadata_offset,
4590
            consume_event_key: consume_event_buffers.as_ref().map(Into::into),
×
4591
        };
4592

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

4628
            let bind_group = render_device.create_bind_group(
×
4629
                "hanabi:bind_group:init:metadata@3",
4630
                layout,
×
4631
                &entries[..],
×
4632
            );
4633

4634
            trace!(
×
4635
                    "Created new metadata@3 bind group for init pass and buffer index {}: effect_metadata=#{}",
×
4636
                    effect_batch.buffer_index,
4637
                    effect_metadata_buffer_table_id.0,
4638
                );
4639

4640
            bind_group
×
4641
        };
4642

4643
        Ok(&self
×
4644
            .init_metadata_bind_groups
×
4645
            .entry(effect_batch.buffer_index)
×
4646
            .and_modify(|cbg| {
×
4647
                if cbg.key != key {
×
4648
                    trace!(
×
4649
                        "Bind group key changed for init metadata@3, re-creating bind group... old={:?} new={:?}",
×
4650
                        cbg.key,
4651
                        key
4652
                    );
4653
                    cbg.key = key;
×
4654
                    cbg.bind_group = make_entry();
×
4655
                }
4656
            })
4657
            .or_insert_with(|| {
×
4658
                trace!("Inserting new bind group for init metadata@3 with key={:?}", key);
×
4659
                CachedBindGroup {
×
4660
                    key,
×
4661
                    bind_group: make_entry(),
×
4662
                }
4663
            })
4664
            .bind_group)
×
4665
    }
4666

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

4684
        // Check arguments consistency
4685
        assert_eq!(effect_batch.child_event_buffers.len(), event_buffers.len());
×
4686
        let emits_gpu_spawn_events = !event_buffers.is_empty();
×
4687
        let child_info_buffer_id = if emits_gpu_spawn_events {
×
4688
            child_info_buffer.as_ref().map(|buffer| buffer.id())
×
4689
        } else {
4690
            // Note: child_info_buffer can be Some() if allocated, but we only consider it
4691
            // if relevant, that is if the effect emits GPU spawn events.
4692
            None
×
4693
        };
4694
        assert_eq!(emits_gpu_spawn_events, child_info_buffer_id.is_some());
×
4695

4696
        let event_buffers_keys = event_buffers
×
4697
            .iter()
4698
            .map(|(_, buffer_binding_source)| buffer_binding_source.into())
×
4699
            .collect::<Vec<_>>();
4700

4701
        let key = UpdateMetadataBindGroupKey {
4702
            buffer_index: effect_batch.buffer_index,
×
4703
            effect_metadata_buffer: effect_metadata_buffer.id(),
×
4704
            effect_metadata_offset: gpu_limits
×
4705
                .effect_metadata_offset(effect_metadata_buffer_table_id.0)
4706
                as u32,
4707
            child_info_buffer_id,
4708
            event_buffers_keys,
4709
        };
4710

4711
        let make_entry = || {
×
4712
            let mut entries = Vec::with_capacity(2 + event_buffers.len());
×
4713
            // @group(3) @binding(0) var<storage, read_write> effect_metadata :
4714
            // EffectMetadata;
4715
            entries.push(BindGroupEntry {
×
4716
                binding: 0,
×
4717
                resource: BindingResource::Buffer(BufferBinding {
×
4718
                    buffer: effect_metadata_buffer,
×
4719
                    offset: key.effect_metadata_offset as u64,
×
4720
                    size: Some(gpu_limits.effect_metadata_aligned_size.into()),
×
4721
                }),
4722
            });
4723
            if emits_gpu_spawn_events {
×
4724
                let child_info_buffer = child_info_buffer.unwrap();
×
4725

4726
                // @group(3) @binding(1) var<storage, read_write> child_info_buffer :
4727
                // ChildInfoBuffer;
4728
                entries.push(BindGroupEntry {
×
4729
                    binding: 1,
×
4730
                    resource: BindingResource::Buffer(BufferBinding {
×
4731
                        buffer: child_info_buffer,
×
4732
                        offset: 0,
×
4733
                        size: None,
×
4734
                    }),
4735
                });
4736

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

4755
            let bind_group = render_device.create_bind_group(
×
4756
                "hanabi:bind_group:update:metadata@3",
4757
                layout,
×
4758
                &entries[..],
×
4759
            );
4760

4761
            trace!(
×
4762
                "Created new metadata@3 bind group for update pass and buffer index {}: effect_metadata={}",
×
4763
                effect_batch.buffer_index,
4764
                effect_metadata_buffer_table_id.0,
4765
            );
4766

4767
            bind_group
×
4768
        };
4769

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

4798
#[derive(SystemParam)]
4799
pub struct QueueEffectsReadOnlyParams<'w, 's> {
4800
    #[cfg(feature = "2d")]
4801
    draw_functions_2d: Res<'w, DrawFunctions<Transparent2d>>,
4802
    #[cfg(feature = "3d")]
4803
    draw_functions_3d: Res<'w, DrawFunctions<Transparent3d>>,
4804
    #[cfg(feature = "3d")]
4805
    draw_functions_alpha_mask: Res<'w, DrawFunctions<AlphaMask3d>>,
4806
    #[cfg(feature = "3d")]
4807
    draw_functions_opaque: Res<'w, DrawFunctions<Opaque3d>>,
4808
    marker: PhantomData<&'s usize>,
4809
}
4810

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

NEW
4829
    for (visible_entities, view, msaa) in views.iter() {
×
4830
        trace!(
×
4831
            "Process new sorted view with {} visible particle effect entities",
×
NEW
4832
            visible_entities.len::<CompiledParticleEffect>()
×
4833
        );
4834

NEW
4835
        let Some(render_phase) = render_phases.get_mut(&view.retained_view_entity) else {
×
4836
            continue;
×
4837
        };
4838

4839
        {
4840
            #[cfg(feature = "trace")]
NEW
4841
            let _span = bevy::log::info_span!("collect_view_entities").entered();
×
4842

4843
            view_entities.clear();
×
4844
            view_entities.extend(
×
4845
                visible_entities
×
NEW
4846
                    .iter::<EffectVisibilityClass>()
×
4847
                    .map(|e| e.1.index() as usize),
×
4848
            );
4849
        }
4850

4851
        // For each view, loop over all the effect batches to determine if the effect
4852
        // needs to be rendered for that view, and enqueue a view-dependent
4853
        // batch if so.
4854
        for (draw_entity, draw_batch) in effect_draw_batches.iter() {
×
4855
            #[cfg(feature = "trace")]
NEW
4856
            let _span_draw = bevy::log::info_span!("draw_batch").entered();
×
4857

4858
            trace!(
×
4859
                "Process draw batch: draw_entity={:?} effect_batch_index={:?}",
×
4860
                draw_entity,
×
4861
                draw_batch.effect_batch_index,
×
4862
            );
4863

4864
            // Get the EffectBatches this EffectDrawBatch is part of.
4865
            let Some(effect_batch) = sorted_effect_batches.get(draw_batch.effect_batch_index)
×
4866
            else {
×
4867
                continue;
×
4868
            };
4869

4870
            trace!(
×
4871
                "-> EffectBach: buffer_index={} spawner_base={} layout_flags={:?}",
×
4872
                effect_batch.buffer_index,
×
4873
                effect_batch.spawner_base,
×
4874
                effect_batch.layout_flags,
×
4875
            );
4876

4877
            // AlphaMask is a binned draw, so no sorted draw can possibly use it
4878
            if effect_batch
×
4879
                .layout_flags
×
4880
                .intersects(LayoutFlags::USE_ALPHA_MASK | LayoutFlags::OPAQUE)
×
4881
            {
4882
                trace!("Non-transparent batch. Skipped.");
×
4883
                continue;
×
4884
            }
4885

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

4905
            // Create and cache the bind group layout for this texture layout
4906
            render_pipeline.cache_material(&effect_batch.texture_layout);
×
4907

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

4911
            let local_space_simulation = effect_batch
×
4912
                .layout_flags
×
4913
                .contains(LayoutFlags::LOCAL_SPACE_SIMULATION);
×
4914
            let alpha_mask = ParticleRenderAlphaMaskPipelineKey::from(effect_batch.layout_flags);
×
4915
            let flipbook = effect_batch.layout_flags.contains(LayoutFlags::FLIPBOOK);
×
4916
            let needs_uv = effect_batch.layout_flags.contains(LayoutFlags::NEEDS_UV);
×
4917
            let needs_normal = effect_batch
×
4918
                .layout_flags
×
4919
                .contains(LayoutFlags::NEEDS_NORMAL);
×
4920
            let ribbons = effect_batch.layout_flags.contains(LayoutFlags::RIBBONS);
×
4921
            let image_count = effect_batch.texture_layout.layout.len() as u8;
×
4922

4923
            // FIXME - Maybe it's better to copy the mesh layout into the batch, instead of
4924
            // re-querying here...?
4925
            let Some(render_mesh) = render_meshes.get(effect_batch.mesh) else {
×
4926
                trace!("Batch has no render mesh, skipped.");
×
4927
                continue;
×
4928
            };
4929
            let mesh_layout = render_mesh.layout.clone();
×
4930

4931
            // Specialize the render pipeline based on the effect batch
4932
            trace!(
×
4933
                "Specializing render pipeline: render_shader={:?} image_count={} alpha_mask={:?} flipbook={:?} hdr={}",
×
4934
                effect_batch.render_shader,
×
4935
                image_count,
×
4936
                alpha_mask,
×
4937
                flipbook,
×
4938
                view.hdr
×
4939
            );
4940

4941
            // Add a draw pass for the effect batch
4942
            trace!("Emitting individual draw for batch");
×
4943

4944
            let alpha_mode = effect_batch.alpha_mode;
×
4945

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

4972
            trace!("+ Render pipeline specialized: id={:?}", render_pipeline_id,);
×
4973
            trace!(
×
4974
                "+ Add Transparent for batch on draw_entity {:?}: buffer_index={} \
×
4975
                spawner_base={} handle={:?}",
×
4976
                draw_entity,
×
4977
                effect_batch.buffer_index,
×
4978
                effect_batch.spawner_base,
×
4979
                effect_batch.handle
×
4980
            );
4981
            render_phase.add(make_phase_item(
×
4982
                render_pipeline_id,
×
4983
                (draw_entity, MainEntity::from(Entity::PLACEHOLDER)),
×
4984
                draw_batch,
×
4985
                view,
×
4986
            ));
4987
        }
4988
    }
4989
}
4990

4991
#[cfg(feature = "3d")]
NEW
4992
fn emit_binned_draw<T, F, G>(
×
4993
    views: &Query<(&RenderVisibleEntities, &ExtractedView, &Msaa)>,
4994
    render_phases: &mut ResMut<ViewBinnedRenderPhases<T>>,
4995
    view_entities: &mut FixedBitSet,
4996
    sorted_effect_batches: &SortedEffectBatches,
4997
    effect_draw_batches: &Query<(Entity, &mut EffectDrawBatch)>,
4998
    render_pipeline: &mut ParticlesRenderPipeline,
4999
    mut specialized_render_pipelines: Mut<SpecializedRenderPipelines<ParticlesRenderPipeline>>,
5000
    pipeline_cache: &PipelineCache,
5001
    render_meshes: &RenderAssets<RenderMesh>,
5002
    make_batch_set_key: F,
5003
    make_bin_key: G,
5004
    #[cfg(all(feature = "2d", feature = "3d"))] pipeline_mode: PipelineMode,
5005
    alpha_mask: ParticleRenderAlphaMaskPipelineKey,
5006
    change_tick: &mut Tick,
5007
) where
5008
    T: BinnedPhaseItem,
5009
    F: Fn(CachedRenderPipelineId, &EffectDrawBatch, &ExtractedView) -> T::BatchSetKey,
5010
    G: Fn() -> T::BinKey,
5011
{
5012
    use bevy::render::render_phase::{BinnedRenderPhaseType, InputUniformIndex};
5013

5014
    trace!("emit_binned_draw() {} views", views.iter().len());
×
5015

NEW
5016
    for (visible_entities, view, msaa) in views.iter() {
×
5017
        trace!("Process new binned view (alpha_mask={:?})", alpha_mask);
×
5018

NEW
5019
        let Some(render_phase) = render_phases.get_mut(&view.retained_view_entity) else {
×
5020
            continue;
×
5021
        };
5022

5023
        {
5024
            #[cfg(feature = "trace")]
NEW
5025
            let _span = bevy::log::info_span!("collect_view_entities").entered();
×
5026

5027
            view_entities.clear();
×
5028
            view_entities.extend(
×
5029
                visible_entities
×
NEW
5030
                    .iter::<EffectVisibilityClass>()
×
5031
                    .map(|e| e.1.index() as usize),
×
5032
            );
5033
        }
5034

5035
        // For each view, loop over all the effect batches to determine if the effect
5036
        // needs to be rendered for that view, and enqueue a view-dependent
5037
        // batch if so.
5038
        for (draw_entity, draw_batch) in effect_draw_batches.iter() {
×
5039
            #[cfg(feature = "trace")]
NEW
5040
            let _span_draw = bevy::log::info_span!("draw_batch").entered();
×
5041

5042
            trace!(
×
5043
                "Process draw batch: draw_entity={:?} effect_batch_index={:?}",
×
5044
                draw_entity,
×
5045
                draw_batch.effect_batch_index,
×
5046
            );
5047

5048
            // Get the EffectBatches this EffectDrawBatch is part of.
5049
            let Some(effect_batch) = sorted_effect_batches.get(draw_batch.effect_batch_index)
×
5050
            else {
×
5051
                continue;
×
5052
            };
5053

5054
            trace!(
×
5055
                "-> EffectBaches: buffer_index={} spawner_base={} layout_flags={:?}",
×
5056
                effect_batch.buffer_index,
×
5057
                effect_batch.spawner_base,
×
5058
                effect_batch.layout_flags,
×
5059
            );
5060

5061
            if ParticleRenderAlphaMaskPipelineKey::from(effect_batch.layout_flags) != alpha_mask {
×
5062
                trace!(
×
5063
                    "Mismatching alpha mask pipeline key (batches={:?}, expected={:?}). Skipped.",
×
5064
                    effect_batch.layout_flags,
×
5065
                    alpha_mask
×
5066
                );
5067
                continue;
×
5068
            }
5069

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

5089
            // Create and cache the bind group layout for this texture layout
5090
            render_pipeline.cache_material(&effect_batch.texture_layout);
×
5091

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

5095
            let local_space_simulation = effect_batch
×
5096
                .layout_flags
×
5097
                .contains(LayoutFlags::LOCAL_SPACE_SIMULATION);
×
5098
            let alpha_mask = ParticleRenderAlphaMaskPipelineKey::from(effect_batch.layout_flags);
×
5099
            let flipbook = effect_batch.layout_flags.contains(LayoutFlags::FLIPBOOK);
×
5100
            let needs_uv = effect_batch.layout_flags.contains(LayoutFlags::NEEDS_UV);
×
5101
            let needs_normal = effect_batch
×
5102
                .layout_flags
×
5103
                .contains(LayoutFlags::NEEDS_NORMAL);
×
5104
            let ribbons = effect_batch.layout_flags.contains(LayoutFlags::RIBBONS);
×
5105
            let image_count = effect_batch.texture_layout.layout.len() as u8;
×
5106
            let render_mesh = render_meshes.get(effect_batch.mesh);
×
5107

5108
            // Specialize the render pipeline based on the effect batch
5109
            trace!(
×
5110
                "Specializing render pipeline: render_shaders={:?} image_count={} alpha_mask={:?} flipbook={:?} hdr={}",
×
5111
                effect_batch.render_shader,
×
5112
                image_count,
×
5113
                alpha_mask,
×
5114
                flipbook,
×
5115
                view.hdr
×
5116
            );
5117

5118
            // Add a draw pass for the effect batch
5119
            trace!("Emitting individual draw for batch");
×
5120

5121
            let alpha_mode = effect_batch.alpha_mode;
×
5122

5123
            let Some(mesh_layout) = render_mesh.map(|gpu_mesh| gpu_mesh.layout.clone()) else {
×
5124
                trace!("Missing mesh vertex buffer layout. Skipped.");
×
5125
                continue;
×
5126
            };
5127

5128
            #[cfg(feature = "trace")]
NEW
5129
            let _span_specialize = bevy::log::info_span!("specialize").entered();
×
5130
            let render_pipeline_id = specialized_render_pipelines.specialize(
×
5131
                pipeline_cache,
×
5132
                render_pipeline,
×
5133
                ParticleRenderPipelineKey {
×
5134
                    shader: effect_batch.render_shader.clone(),
×
5135
                    mesh_layout: Some(mesh_layout),
×
5136
                    particle_layout: effect_batch.particle_layout.clone(),
×
5137
                    texture_layout: effect_batch.texture_layout.clone(),
×
5138
                    local_space_simulation,
×
5139
                    alpha_mask,
×
5140
                    alpha_mode,
×
5141
                    flipbook,
×
5142
                    needs_uv,
×
5143
                    needs_normal,
×
5144
                    ribbons,
×
5145
                    #[cfg(all(feature = "2d", feature = "3d"))]
×
5146
                    pipeline_mode,
×
5147
                    msaa_samples: msaa.samples(),
×
5148
                    hdr: view.hdr,
×
5149
                },
5150
            );
5151
            #[cfg(feature = "trace")]
5152
            _span_specialize.exit();
×
5153

5154
            trace!("+ Render pipeline specialized: id={:?}", render_pipeline_id,);
×
5155
            trace!(
×
5156
                "+ Add Transparent for batch on draw_entity {:?}: buffer_index={} \
×
5157
                spawner_base={} handle={:?}",
×
5158
                draw_entity,
×
5159
                effect_batch.buffer_index,
×
5160
                effect_batch.spawner_base,
×
5161
                effect_batch.handle
×
5162
            );
5163
            render_phase.add(
×
NEW
5164
                make_batch_set_key(render_pipeline_id, draw_batch, view),
×
NEW
5165
                make_bin_key(),
×
NEW
5166
                (draw_entity, draw_batch.main_entity),
×
NEW
5167
                InputUniformIndex::default(),
×
UNCOV
5168
                BinnedRenderPhaseType::NonMesh,
×
NEW
5169
                *change_tick,
×
5170
            );
5171
        }
5172
    }
5173
}
5174

5175
#[allow(clippy::too_many_arguments)]
5176
pub(crate) fn queue_effects(
×
5177
    views: Query<(&RenderVisibleEntities, &ExtractedView, &Msaa)>,
5178
    effects_meta: Res<EffectsMeta>,
5179
    mut render_pipeline: ResMut<ParticlesRenderPipeline>,
5180
    mut specialized_render_pipelines: ResMut<SpecializedRenderPipelines<ParticlesRenderPipeline>>,
5181
    pipeline_cache: Res<PipelineCache>,
5182
    mut effect_bind_groups: ResMut<EffectBindGroups>,
5183
    sorted_effect_batches: Res<SortedEffectBatches>,
5184
    effect_draw_batches: Query<(Entity, &mut EffectDrawBatch)>,
5185
    events: Res<EffectAssetEvents>,
5186
    render_meshes: Res<RenderAssets<RenderMesh>>,
5187
    read_params: QueueEffectsReadOnlyParams,
5188
    mut view_entities: Local<FixedBitSet>,
5189
    #[cfg(feature = "2d")] mut transparent_2d_render_phases: ResMut<
5190
        ViewSortedRenderPhases<Transparent2d>,
5191
    >,
5192
    #[cfg(feature = "3d")] mut transparent_3d_render_phases: ResMut<
5193
        ViewSortedRenderPhases<Transparent3d>,
5194
    >,
5195
    #[cfg(feature = "3d")] (mut opaque_3d_render_phases, mut alpha_mask_3d_render_phases): (
5196
        ResMut<ViewBinnedRenderPhases<Opaque3d>>,
5197
        ResMut<ViewBinnedRenderPhases<AlphaMask3d>>,
5198
    ),
5199
    mut change_tick: Local<Tick>,
5200
) {
5201
    #[cfg(feature = "trace")]
NEW
5202
    let _span = bevy::log::info_span!("hanabi:queue_effects").entered();
×
5203

5204
    trace!("queue_effects");
×
5205

5206
    // Bump the change tick so that Bevy is forced to rebuild the binned render
5207
    // phase bins. We don't use the built-in caching so we don't want Bevy to
5208
    // reuse stale data.
NEW
5209
    let next_change_tick = change_tick.get() + 1;
×
NEW
5210
    change_tick.set(next_change_tick);
×
5211

5212
    // If an image has changed, the GpuImage has (probably) changed
5213
    for event in &events.images {
×
5214
        match event {
5215
            AssetEvent::Added { .. } => None,
×
5216
            AssetEvent::LoadedWithDependencies { .. } => None,
×
5217
            AssetEvent::Unused { .. } => None,
×
5218
            AssetEvent::Modified { id } => {
×
5219
                trace!("Destroy bind group of modified image asset {:?}", id);
×
5220
                effect_bind_groups.images.remove(id)
×
5221
            }
5222
            AssetEvent::Removed { id } => {
×
5223
                trace!("Destroy bind group of removed image asset {:?}", id);
×
5224
                effect_bind_groups.images.remove(id)
×
5225
            }
5226
        };
5227
    }
5228

5229
    if effects_meta.spawner_buffer.buffer().is_none() || effects_meta.spawner_buffer.is_empty() {
×
5230
        // No spawners are active
5231
        return;
×
5232
    }
5233

5234
    // Loop over all 2D cameras/views that need to render effects
5235
    #[cfg(feature = "2d")]
5236
    {
5237
        #[cfg(feature = "trace")]
5238
        let _span_draw = bevy::log::info_span!("draw_2d").entered();
5239

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

5246
        // Effects with full alpha blending
5247
        if !views.is_empty() {
5248
            trace!("Emit effect draw calls for alpha blended 2D views...");
×
5249
            emit_sorted_draw(
5250
                &views,
5251
                &mut transparent_2d_render_phases,
5252
                &mut view_entities,
5253
                &sorted_effect_batches,
5254
                &effect_draw_batches,
5255
                &mut render_pipeline,
5256
                specialized_render_pipelines.reborrow(),
5257
                &render_meshes,
5258
                &pipeline_cache,
5259
                |id, entity, draw_batch, _view| Transparent2d {
×
5260
                    sort_key: FloatOrd(draw_batch.translation.z),
×
5261
                    entity,
×
5262
                    pipeline: id,
×
5263
                    draw_function: draw_effects_function_2d,
×
5264
                    batch_range: 0..1,
×
NEW
5265
                    extracted_index: 0, // ???
×
NEW
5266
                    extra_index: PhaseItemExtraIndex::None,
×
NEW
5267
                    indexed: true, // ???
×
5268
                },
5269
                #[cfg(feature = "3d")]
5270
                PipelineMode::Camera2d,
5271
            );
5272
        }
5273
    }
5274

5275
    // Loop over all 3D cameras/views that need to render effects
5276
    #[cfg(feature = "3d")]
5277
    {
5278
        #[cfg(feature = "trace")]
5279
        let _span_draw = bevy::log::info_span!("draw_3d").entered();
5280

5281
        // Effects with full alpha blending
5282
        if !views.is_empty() {
5283
            trace!("Emit effect draw calls for alpha blended 3D views...");
×
5284

5285
            let draw_effects_function_3d = read_params
5286
                .draw_functions_3d
5287
                .read()
5288
                .get_id::<DrawEffects>()
5289
                .unwrap();
5290

5291
            emit_sorted_draw(
5292
                &views,
5293
                &mut transparent_3d_render_phases,
5294
                &mut view_entities,
5295
                &sorted_effect_batches,
5296
                &effect_draw_batches,
5297
                &mut render_pipeline,
5298
                specialized_render_pipelines.reborrow(),
5299
                &render_meshes,
5300
                &pipeline_cache,
5301
                |id, entity, batch, view| Transparent3d {
×
5302
                    distance: view
×
5303
                        .rangefinder3d()
×
5304
                        .distance_translation(&batch.translation),
×
NEW
5305
                    pipeline: id,
×
NEW
5306
                    entity,
×
NEW
5307
                    draw_function: draw_effects_function_3d,
×
5308
                    batch_range: 0..1,
×
NEW
5309
                    extra_index: PhaseItemExtraIndex::None,
×
NEW
5310
                    indexed: true, // ???
×
5311
                },
5312
                #[cfg(feature = "2d")]
5313
                PipelineMode::Camera3d,
5314
            );
5315
        }
5316

5317
        // Effects with alpha mask
5318
        if !views.is_empty() {
5319
            #[cfg(feature = "trace")]
NEW
5320
            let _span_draw = bevy::log::info_span!("draw_alphamask").entered();
×
5321

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

5324
            let draw_effects_function_alpha_mask = read_params
5325
                .draw_functions_alpha_mask
5326
                .read()
5327
                .get_id::<DrawEffects>()
5328
                .unwrap();
5329

5330
            emit_binned_draw(
5331
                &views,
5332
                &mut alpha_mask_3d_render_phases,
5333
                &mut view_entities,
5334
                &sorted_effect_batches,
5335
                &effect_draw_batches,
5336
                &mut render_pipeline,
5337
                specialized_render_pipelines.reborrow(),
5338
                &pipeline_cache,
5339
                &render_meshes,
NEW
5340
                |id, _batch, _view| OpaqueNoLightmap3dBatchSetKey {
×
5341
                    pipeline: id,
×
5342
                    draw_function: draw_effects_function_alpha_mask,
×
NEW
5343
                    material_bind_group_index: None,
×
NEW
5344
                    vertex_slab: default(),
×
NEW
5345
                    index_slab: None,
×
5346
                },
5347
                // Unused for now
NEW
5348
                || OpaqueNoLightmap3dBinKey {
×
NEW
5349
                    asset_id: AssetId::<Mesh>::invalid().untyped(),
×
5350
                },
5351
                #[cfg(feature = "2d")]
5352
                PipelineMode::Camera3d,
5353
                ParticleRenderAlphaMaskPipelineKey::AlphaMask,
5354
                &mut change_tick,
5355
            );
5356
        }
5357

5358
        // Opaque particles
5359
        if !views.is_empty() {
5360
            #[cfg(feature = "trace")]
NEW
5361
            let _span_draw = bevy::log::info_span!("draw_opaque").entered();
×
5362

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

5365
            let draw_effects_function_opaque = read_params
5366
                .draw_functions_opaque
5367
                .read()
5368
                .get_id::<DrawEffects>()
5369
                .unwrap();
5370

5371
            emit_binned_draw(
5372
                &views,
5373
                &mut opaque_3d_render_phases,
5374
                &mut view_entities,
5375
                &sorted_effect_batches,
5376
                &effect_draw_batches,
5377
                &mut render_pipeline,
5378
                specialized_render_pipelines.reborrow(),
5379
                &pipeline_cache,
5380
                &render_meshes,
NEW
5381
                |id, _batch, _view| Opaque3dBatchSetKey {
×
5382
                    pipeline: id,
×
5383
                    draw_function: draw_effects_function_opaque,
×
NEW
5384
                    material_bind_group_index: None,
×
NEW
5385
                    vertex_slab: default(),
×
NEW
5386
                    index_slab: None,
×
NEW
5387
                    lightmap_slab: None,
×
5388
                },
5389
                // Unused for now
NEW
5390
                || Opaque3dBinKey {
×
NEW
5391
                    asset_id: AssetId::<Mesh>::invalid().untyped(),
×
5392
                },
5393
                #[cfg(feature = "2d")]
5394
                PipelineMode::Camera3d,
5395
                ParticleRenderAlphaMaskPipelineKey::Opaque,
5396
                &mut change_tick,
5397
            );
5398
        }
5399
    }
5400
}
5401

5402
/// Prepare GPU resources for effect rendering.
5403
///
5404
/// This system runs in the [`RenderSet::PrepareResources`] render set, after
5405
/// Bevy has updated the [`ViewUniforms`], which need to be referenced to get
5406
/// access to the current camera view.
5407
pub(crate) fn prepare_gpu_resources(
×
5408
    mut effects_meta: ResMut<EffectsMeta>,
5409
    //mut effect_cache: ResMut<EffectCache>,
5410
    mut event_cache: ResMut<EventCache>,
5411
    mut effect_bind_groups: ResMut<EffectBindGroups>,
5412
    mut sort_bind_groups: ResMut<SortBindGroups>,
5413
    render_device: Res<RenderDevice>,
5414
    render_queue: Res<RenderQueue>,
5415
    view_uniforms: Res<ViewUniforms>,
5416
    render_pipeline: Res<ParticlesRenderPipeline>,
5417
) {
5418
    // Get the binding for the ViewUniform, the uniform data structure containing
5419
    // the Camera data for the current view. If not available, we cannot render
5420
    // anything.
5421
    let Some(view_binding) = view_uniforms.uniforms.binding() else {
×
5422
        return;
×
5423
    };
5424

5425
    // Create the bind group for the camera/view parameters
5426
    // FIXME - Not here!
5427
    effects_meta.view_bind_group = Some(render_device.create_bind_group(
5428
        "hanabi:bind_group_camera_view",
5429
        &render_pipeline.view_layout,
5430
        &[
5431
            BindGroupEntry {
5432
                binding: 0,
5433
                resource: view_binding,
5434
            },
5435
            BindGroupEntry {
5436
                binding: 1,
5437
                resource: effects_meta.sim_params_uniforms.binding().unwrap(),
5438
            },
5439
        ],
5440
    ));
5441

5442
    // Re-/allocate any GPU buffer if needed
5443
    //effect_cache.prepare_buffers(&render_device, &render_queue, &mut
5444
    // effect_bind_groups);
5445
    event_cache.prepare_buffers(&render_device, &render_queue, &mut effect_bind_groups);
5446
    sort_bind_groups.prepare_buffers(&render_device);
5447
    if effects_meta
5448
        .update_dispatch_indirect_buffer
5449
        .prepare_buffers(&render_device)
5450
    {
5451
        // All those bind groups use the buffer so need to be re-created
5452
        trace!("*** Dispatch indirect buffer for update pass re-allocated; clearing all bind groups using it.");
×
5453
        effect_bind_groups.particle_buffers.clear();
×
5454
    }
5455
}
5456

5457
/// Read the queued init fill dispatch operations, batch them together by
5458
/// contiguous source and destination entries in the buffers, and enqueue
5459
/// corresponding GPU buffer fill dispatch operations for all batches.
5460
///
5461
/// This system runs after the GPU buffers have been (re-)allocated in
5462
/// [`prepare_gpu_resources()`], so that it can read the new buffer IDs and
5463
/// reference them from the generic [`GpuBufferOperationQueue`].
5464
pub(crate) fn queue_init_fill_dispatch_ops(
×
5465
    event_cache: Res<EventCache>,
5466
    render_device: Res<RenderDevice>,
5467
    render_queue: Res<RenderQueue>,
5468
    mut init_fill_dispatch_queue: ResMut<InitFillDispatchQueue>,
5469
    mut gpu_buffer_operations: ResMut<GpuBufferOperations>,
5470
) {
5471
    // Submit all queued init fill dispatch operations with the proper buffers
5472
    if !init_fill_dispatch_queue.is_empty() {
×
5473
        let src_buffer = event_cache.child_infos().buffer();
×
5474
        let dst_buffer = event_cache.init_indirect_dispatch_buffer();
×
5475
        if let (Some(src_buffer), Some(dst_buffer)) = (src_buffer, dst_buffer) {
×
5476
            init_fill_dispatch_queue.submit(src_buffer, dst_buffer, &mut gpu_buffer_operations);
5477
        } else {
5478
            if src_buffer.is_none() {
×
5479
                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());
×
5480
            }
5481
            if dst_buffer.is_none() {
×
5482
                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());
×
5483
            }
5484
        }
5485
    }
5486

5487
    // Once all GPU operations for this frame are enqueued, upload them to GPU
5488
    gpu_buffer_operations.end_frame(&render_device, &render_queue);
×
5489
}
5490

5491
pub(crate) fn prepare_bind_groups(
×
5492
    mut effects_meta: ResMut<EffectsMeta>,
5493
    mut effect_cache: ResMut<EffectCache>,
5494
    mut event_cache: ResMut<EventCache>,
5495
    mut effect_bind_groups: ResMut<EffectBindGroups>,
5496
    mut property_bind_groups: ResMut<PropertyBindGroups>,
5497
    mut sort_bind_groups: ResMut<SortBindGroups>,
5498
    property_cache: Res<PropertyCache>,
5499
    sorted_effect_batched: Res<SortedEffectBatches>,
5500
    render_device: Res<RenderDevice>,
5501
    dispatch_indirect_pipeline: Res<DispatchIndirectPipeline>,
5502
    utils_pipeline: Res<UtilsPipeline>,
5503
    update_pipeline: Res<ParticlesUpdatePipeline>,
5504
    render_pipeline: ResMut<ParticlesRenderPipeline>,
5505
    gpu_images: Res<RenderAssets<GpuImage>>,
5506
    mut gpu_buffer_operation_queue: ResMut<GpuBufferOperations>,
5507
) {
5508
    // We can't simulate nor render anything without at least the spawner buffer
5509
    if effects_meta.spawner_buffer.is_empty() {
×
5510
        return;
×
5511
    }
5512
    let Some(spawner_buffer) = effects_meta.spawner_buffer.buffer().cloned() else {
×
5513
        return;
×
5514
    };
5515

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

5521
    {
5522
        #[cfg(feature = "trace")]
5523
        let _span = bevy::log::info_span!("shared_bind_groups").entered();
5524

5525
        // Make a copy of the buffer IDs before borrowing effects_meta mutably in the
5526
        // loop below. Also allows earlying out before doing any work in case some
5527
        // buffer is missing.
5528
        let Some(spawner_buffer) = effects_meta.spawner_buffer.buffer().cloned() else {
×
5529
            return;
×
5530
        };
5531

5532
        // Create the sim_params@0 bind group for the global simulation parameters,
5533
        // which is shared by the init and update passes.
5534
        if effects_meta.indirect_sim_params_bind_group.is_none() {
×
5535
            effects_meta.indirect_sim_params_bind_group = Some(render_device.create_bind_group(
×
5536
                "hanabi:bind_group:vfx_indirect:sim_params@0",
×
5537
                &update_pipeline.sim_params_layout, // FIXME - Shared with init
×
5538
                &[BindGroupEntry {
×
5539
                    binding: 0,
×
5540
                    resource: effects_meta.sim_params_uniforms.binding().unwrap(),
×
5541
                }],
5542
            ));
5543
        }
5544

5545
        // Create the @1 bind group for the indirect dispatch preparation pass of all
5546
        // effects at once
5547
        effects_meta.indirect_metadata_bind_group = match (
5548
            effects_meta.effect_metadata_buffer.buffer(),
5549
            effects_meta.update_dispatch_indirect_buffer.buffer(),
5550
        ) {
5551
            (Some(effect_metadata_buffer), Some(dispatch_indirect_buffer)) => {
×
5552
                // Base bind group for indirect pass
5553
                Some(render_device.create_bind_group(
×
5554
                    "hanabi:bind_group:vfx_indirect:metadata@1",
×
5555
                    &dispatch_indirect_pipeline.effect_metadata_bind_group_layout,
×
5556
                    &[
×
5557
                        // @group(1) @binding(0) var<storage, read_write> effect_metadata_buffer : array<u32>;
5558
                        BindGroupEntry {
×
5559
                            binding: 0,
×
5560
                            resource: BindingResource::Buffer(BufferBinding {
×
5561
                                buffer: effect_metadata_buffer,
×
5562
                                offset: 0,
×
5563
                                size: None, //NonZeroU64::new(256), // Some(GpuEffectMetadata::min_size()),
×
5564
                            }),
5565
                        },
5566
                        // @group(1) @binding(1) var<storage, read_write> dispatch_indirect_buffer : array<u32>;
5567
                        BindGroupEntry {
×
5568
                            binding: 1,
×
5569
                            resource: BindingResource::Buffer(BufferBinding {
×
5570
                                buffer: dispatch_indirect_buffer,
×
5571
                                offset: 0,
×
5572
                                size: None, //NonZeroU64::new(256), // Some(GpuDispatchIndirect::min_size()),
×
5573
                            }),
5574
                        },
5575
                    ],
5576
                ))
5577
            }
5578

5579
            // Some buffer is not yet available, can't create the bind group
5580
            _ => None,
×
5581
        };
5582

5583
        // Create the @2 bind group for the indirect dispatch preparation pass of all
5584
        // effects at once
5585
        if effects_meta.indirect_spawner_bind_group.is_none() {
×
5586
            let bind_group = render_device.create_bind_group(
×
5587
                "hanabi:bind_group:vfx_indirect:spawner@2",
5588
                &dispatch_indirect_pipeline.spawner_bind_group_layout,
×
5589
                &[
×
5590
                    // @group(2) @binding(0) var<storage, read> spawner_buffer : array<Spawner>;
5591
                    BindGroupEntry {
×
5592
                        binding: 0,
×
5593
                        resource: BindingResource::Buffer(BufferBinding {
×
5594
                            buffer: &spawner_buffer,
×
5595
                            offset: 0,
×
5596
                            size: None,
×
5597
                        }),
5598
                    },
5599
                ],
5600
            );
5601

5602
            effects_meta.indirect_spawner_bind_group = Some(bind_group);
×
5603
        }
5604
    }
5605

5606
    // Create the per-buffer bind groups
5607
    trace!("Create per-buffer bind groups...");
×
5608
    for (buffer_index, effect_buffer) in effect_cache.buffers().iter().enumerate() {
×
5609
        #[cfg(feature = "trace")]
5610
        let _span_buffer = bevy::log::info_span!("create_buffer_bind_groups").entered();
5611

5612
        let Some(effect_buffer) = effect_buffer else {
×
5613
            trace!(
×
5614
                "Effect buffer index #{} has no allocated EffectBuffer, skipped.",
×
5615
                buffer_index
5616
            );
5617
            continue;
×
5618
        };
5619

5620
        // Ensure all effects in this batch have a bind group for the entire buffer of
5621
        // the group, since the update phase runs on an entire group/buffer at once,
5622
        // with all the effect instances in it batched together.
5623
        trace!("effect particle buffer_index=#{}", buffer_index);
×
5624
        effect_bind_groups
5625
            .particle_buffers
5626
            .entry(buffer_index as u32)
5627
            .or_insert_with(|| {
×
5628
                // Bind group particle@1 for render pass
5629
                trace!("Creating particle@1 bind group for buffer #{buffer_index} in render pass");
×
5630
                let spawner_min_binding_size = GpuSpawnerParams::aligned_size(
×
5631
                    render_device.limits().min_storage_buffer_offset_alignment,
×
5632
                );
5633
                let entries = [
×
5634
                    // @group(1) @binding(0) var<storage, read> particle_buffer : ParticleBuffer;
5635
                    BindGroupEntry {
×
5636
                        binding: 0,
×
5637
                        resource: effect_buffer.max_binding(),
×
5638
                    },
5639
                    // @group(1) @binding(1) var<storage, read> indirect_buffer : IndirectBuffer;
5640
                    BindGroupEntry {
×
5641
                        binding: 1,
×
5642
                        resource: effect_buffer.indirect_index_max_binding(),
×
5643
                    },
5644
                    // @group(1) @binding(2) var<storage, read> spawner : Spawner;
5645
                    BindGroupEntry {
×
5646
                        binding: 2,
×
5647
                        resource: BindingResource::Buffer(BufferBinding {
×
5648
                            buffer: &spawner_buffer,
×
5649
                            offset: 0,
×
5650
                            size: Some(spawner_min_binding_size),
×
5651
                        }),
5652
                    },
5653
                ];
5654
                let render = render_device.create_bind_group(
×
5655
                    &format!("hanabi:bind_group:render:particles@1:vfx{buffer_index}")[..],
×
5656
                    effect_buffer.render_particles_buffer_layout(),
×
5657
                    &entries[..],
×
5658
                );
5659

5660
                BufferBindGroups { render }
×
5661
            });
5662
    }
5663

5664
    // Create bind groups for queued GPU buffer operations
5665
    gpu_buffer_operation_queue.create_bind_groups(&render_device, &utils_pipeline);
5666

5667
    // Create the per-effect bind groups
5668
    let spawner_buffer_binding_size =
5669
        NonZeroU64::new(effects_meta.spawner_buffer.aligned_size() as u64).unwrap();
5670
    for effect_batch in sorted_effect_batched.iter() {
×
5671
        #[cfg(feature = "trace")]
NEW
5672
        let _span_buffer = bevy::log::info_span!("create_batch_bind_groups").entered();
×
5673

5674
        // Create the property bind group @2 if needed
5675
        if let Some(property_key) = &effect_batch.property_key {
×
5676
            if let Err(err) = property_bind_groups.ensure_exists(
×
5677
                property_key,
5678
                &property_cache,
5679
                &spawner_buffer,
5680
                spawner_buffer_binding_size,
5681
                &render_device,
5682
            ) {
5683
                error!("Failed to create property bind group for effect batch: {err:?}");
×
5684
                continue;
5685
            }
5686
        } else if let Err(err) = property_bind_groups.ensure_exists_no_property(
×
5687
            &property_cache,
×
5688
            &spawner_buffer,
×
5689
            spawner_buffer_binding_size,
×
5690
            &render_device,
×
5691
        ) {
5692
            error!("Failed to create property bind group for effect batch: {err:?}");
×
5693
            continue;
5694
        }
5695

5696
        // Bind group particle@1 for the simulate compute shaders (init and udpate) to
5697
        // simulate particles.
5698
        if effect_cache
×
5699
            .create_particle_sim_bind_group(
5700
                effect_batch.buffer_index,
×
5701
                &render_device,
×
5702
                effect_batch.particle_layout.min_binding_size32(),
×
5703
                effect_batch.parent_min_binding_size,
×
5704
                effect_batch.parent_binding_source.as_ref(),
×
5705
            )
5706
            .is_err()
5707
        {
5708
            error!("No particle buffer allocated for effect batch.");
×
5709
            continue;
×
5710
        }
5711

5712
        // Bind group @3 of init pass
5713
        // FIXME - this is instance-dependent, not buffer-dependent
5714
        {
5715
            let consume_gpu_spawn_events = effect_batch
×
5716
                .layout_flags
×
5717
                .contains(LayoutFlags::CONSUME_GPU_SPAWN_EVENTS);
×
5718
            let consume_event_buffers = if let BatchSpawnInfo::GpuSpawner { .. } =
×
5719
                effect_batch.spawn_info
5720
            {
5721
                assert!(consume_gpu_spawn_events);
×
5722
                let cached_effect_events = effect_batch.cached_effect_events.as_ref().unwrap();
×
5723
                Some(ConsumeEventBuffers {
×
5724
                    child_infos_buffer: event_cache.child_infos_buffer().unwrap(),
×
5725
                    events: BufferSlice {
×
5726
                        buffer: event_cache
×
5727
                            .get_buffer(cached_effect_events.buffer_index)
×
5728
                            .unwrap(),
×
5729
                        // Note: event range is in u32 count, not bytes
5730
                        offset: cached_effect_events.range.start * 4,
×
5731
                        size: NonZeroU32::new(cached_effect_events.range.len() as u32 * 4).unwrap(),
×
5732
                    },
5733
                })
5734
            } else {
5735
                assert!(!consume_gpu_spawn_events);
×
5736
                None
×
5737
            };
5738
            let Some(init_metadata_layout) =
×
5739
                effect_cache.metadata_init_bind_group_layout(consume_gpu_spawn_events)
5740
            else {
5741
                continue;
×
5742
            };
5743
            if effect_bind_groups
5744
                .get_or_create_init_metadata(
5745
                    effect_batch,
5746
                    &effects_meta.gpu_limits,
5747
                    &render_device,
5748
                    init_metadata_layout,
5749
                    effects_meta.effect_metadata_buffer.buffer().unwrap(),
5750
                    consume_event_buffers,
5751
                )
5752
                .is_err()
5753
            {
5754
                continue;
×
5755
            }
5756
        }
5757

5758
        // Bind group @3 of update pass
5759
        // FIXME - this is instance-dependent, not buffer-dependent#
5760
        {
5761
            let num_event_buffers = effect_batch.child_event_buffers.len() as u32;
×
5762

5763
            let Some(update_metadata_layout) =
×
5764
                effect_cache.metadata_update_bind_group_layout(num_event_buffers)
5765
            else {
5766
                continue;
×
5767
            };
5768
            if effect_bind_groups
5769
                .get_or_create_update_metadata(
5770
                    effect_batch,
5771
                    &effects_meta.gpu_limits,
5772
                    &render_device,
5773
                    update_metadata_layout,
5774
                    effects_meta.effect_metadata_buffer.buffer().unwrap(),
5775
                    event_cache.child_infos_buffer(),
5776
                    &effect_batch.child_event_buffers[..],
5777
                )
5778
                .is_err()
5779
            {
5780
                continue;
×
5781
            }
5782
        }
5783

5784
        if effect_batch.layout_flags.contains(LayoutFlags::RIBBONS) {
×
5785
            let effect_buffer = effect_cache.get_buffer(effect_batch.buffer_index).unwrap();
×
5786

5787
            // Bind group @0 of sort-fill pass
5788
            let particle_buffer = effect_buffer.particle_buffer();
×
5789
            let indirect_index_buffer = effect_buffer.indirect_index_buffer();
×
5790
            let effect_metadata_buffer = effects_meta.effect_metadata_buffer.buffer().unwrap();
×
5791
            if let Err(err) = sort_bind_groups.ensure_sort_fill_bind_group(
×
5792
                &effect_batch.particle_layout,
×
5793
                particle_buffer,
×
5794
                indirect_index_buffer,
×
5795
                effect_metadata_buffer,
×
5796
            ) {
5797
                error!(
5798
                    "Failed to create sort-fill bind group @0 for ribbon effect: {:?}",
×
5799
                    err
5800
                );
5801
                continue;
5802
            }
5803

5804
            // Bind group @0 of sort-copy pass
5805
            let indirect_index_buffer = effect_buffer.indirect_index_buffer();
×
5806
            if let Err(err) = sort_bind_groups
×
5807
                .ensure_sort_copy_bind_group(indirect_index_buffer, effect_metadata_buffer)
×
5808
            {
5809
                error!(
5810
                    "Failed to create sort-copy bind group @0 for ribbon effect: {:?}",
×
5811
                    err
5812
                );
5813
                continue;
5814
            }
5815
        }
5816

5817
        // Ensure the particle texture(s) are available as GPU resources and that a bind
5818
        // group for them exists
5819
        // FIXME fix this insert+get below
5820
        if !effect_batch.texture_layout.layout.is_empty() {
×
5821
            // This should always be available, as this is cached into the render pipeline
5822
            // just before we start specializing it.
5823
            let Some(material_bind_group_layout) =
×
5824
                render_pipeline.get_material(&effect_batch.texture_layout)
×
5825
            else {
5826
                error!(
×
5827
                    "Failed to find material bind group layout for buffer #{}",
×
5828
                    effect_batch.buffer_index
5829
                );
5830
                continue;
×
5831
            };
5832

5833
            // TODO = move
5834
            let material = Material {
5835
                layout: effect_batch.texture_layout.clone(),
5836
                textures: effect_batch.textures.iter().map(|h| h.id()).collect(),
×
5837
            };
5838
            assert_eq!(material.layout.layout.len(), material.textures.len());
5839

5840
            //let bind_group_entries = material.make_entries(&gpu_images).unwrap();
5841
            let Ok(bind_group_entries) = material.make_entries(&gpu_images) else {
×
5842
                trace!(
×
5843
                    "Temporarily ignoring material {:?} due to missing image(s)",
×
5844
                    material
5845
                );
5846
                continue;
×
5847
            };
5848

5849
            effect_bind_groups
5850
                .material_bind_groups
5851
                .entry(material.clone())
5852
                .or_insert_with(|| {
×
5853
                    debug!("Creating material bind group for material {:?}", material);
×
5854
                    render_device.create_bind_group(
×
5855
                        &format!(
×
5856
                            "hanabi:material_bind_group_{}",
×
5857
                            material.layout.layout.len()
×
5858
                        )[..],
×
5859
                        material_bind_group_layout,
×
5860
                        &bind_group_entries[..],
×
5861
                    )
5862
                });
5863
        }
5864
    }
5865
}
5866

5867
type DrawEffectsSystemState = SystemState<(
5868
    SRes<EffectsMeta>,
5869
    SRes<EffectBindGroups>,
5870
    SRes<PipelineCache>,
5871
    SRes<RenderAssets<RenderMesh>>,
5872
    SRes<MeshAllocator>,
5873
    SQuery<Read<ViewUniformOffset>>,
5874
    SRes<SortedEffectBatches>,
5875
    SQuery<Read<EffectDrawBatch>>,
5876
)>;
5877

5878
/// Draw function for rendering all active effects for the current frame.
5879
///
5880
/// Effects are rendered in the [`Transparent2d`] phase of the main 2D pass,
5881
/// and the [`Transparent3d`] phase of the main 3D pass.
5882
pub(crate) struct DrawEffects {
5883
    params: DrawEffectsSystemState,
5884
}
5885

5886
impl DrawEffects {
5887
    pub fn new(world: &mut World) -> Self {
×
5888
        Self {
5889
            params: SystemState::new(world),
×
5890
        }
5891
    }
5892
}
5893

5894
/// Draw all particles of a single effect in view, in 2D or 3D.
5895
///
5896
/// FIXME: use pipeline ID to look up which group index it is.
5897
fn draw<'w>(
×
5898
    world: &'w World,
5899
    pass: &mut TrackedRenderPass<'w>,
5900
    view: Entity,
5901
    entity: (Entity, MainEntity),
5902
    pipeline_id: CachedRenderPipelineId,
5903
    params: &mut DrawEffectsSystemState,
5904
) {
5905
    let (
×
5906
        effects_meta,
×
5907
        effect_bind_groups,
×
5908
        pipeline_cache,
×
5909
        meshes,
×
5910
        mesh_allocator,
×
5911
        views,
×
5912
        sorted_effect_batches,
×
5913
        effect_draw_batches,
×
5914
    ) = params.get(world);
×
5915
    let view_uniform = views.get(view).unwrap();
×
5916
    let effects_meta = effects_meta.into_inner();
×
5917
    let effect_bind_groups = effect_bind_groups.into_inner();
×
5918
    let meshes = meshes.into_inner();
×
5919
    let mesh_allocator = mesh_allocator.into_inner();
×
5920
    let effect_draw_batch = effect_draw_batches.get(entity.0).unwrap();
×
5921
    let effect_batch = sorted_effect_batches
×
5922
        .get(effect_draw_batch.effect_batch_index)
×
5923
        .unwrap();
5924

5925
    let gpu_limits = &effects_meta.gpu_limits;
×
5926

5927
    let Some(pipeline) = pipeline_cache.into_inner().get_render_pipeline(pipeline_id) else {
×
5928
        return;
×
5929
    };
5930

5931
    trace!("render pass");
×
5932

5933
    pass.set_render_pipeline(pipeline);
×
5934

5935
    let Some(render_mesh): Option<&RenderMesh> = meshes.get(effect_batch.mesh) else {
×
5936
        return;
×
5937
    };
5938
    let Some(vertex_buffer_slice) = mesh_allocator.mesh_vertex_slice(&effect_batch.mesh) else {
×
5939
        return;
×
5940
    };
5941

5942
    // Vertex buffer containing the particle model to draw. Generally a quad.
5943
    // FIXME - need to upload "vertex_buffer_slice.range.start as i32" into
5944
    // "base_vertex" in the indirect struct...
5945
    pass.set_vertex_buffer(0, vertex_buffer_slice.buffer.slice(..));
×
5946

5947
    // View properties (camera matrix, etc.)
5948
    pass.set_bind_group(
×
5949
        0,
5950
        effects_meta.view_bind_group.as_ref().unwrap(),
×
5951
        &[view_uniform.offset],
×
5952
    );
5953

5954
    // Particles buffer
5955
    let spawner_base = effect_batch.spawner_base;
×
5956
    let spawner_buffer_aligned = effects_meta.spawner_buffer.aligned_size();
×
5957
    assert!(spawner_buffer_aligned >= GpuSpawnerParams::min_size().get() as usize);
×
5958
    let spawner_offset = spawner_base * spawner_buffer_aligned as u32;
×
5959
    pass.set_bind_group(
×
5960
        1,
5961
        effect_bind_groups
×
5962
            .particle_render(effect_batch.buffer_index)
×
5963
            .unwrap(),
×
5964
        &[spawner_offset],
×
5965
    );
5966

5967
    // Particle texture
5968
    // TODO = move
5969
    let material = Material {
5970
        layout: effect_batch.texture_layout.clone(),
×
5971
        textures: effect_batch.textures.iter().map(|h| h.id()).collect(),
×
5972
    };
5973
    if !effect_batch.texture_layout.layout.is_empty() {
×
5974
        if let Some(bind_group) = effect_bind_groups.material_bind_groups.get(&material) {
×
5975
            pass.set_bind_group(2, bind_group, &[]);
×
5976
        } else {
5977
            // Texture(s) not ready; skip this drawing for now
5978
            trace!(
×
5979
                "Particle material bind group not available for batch buf={}. Skipping draw call.",
×
5980
                effect_batch.buffer_index,
×
5981
            );
5982
            return;
×
5983
        }
5984
    }
5985

5986
    let effect_metadata_index = effect_batch
×
5987
        .dispatch_buffer_indices
×
5988
        .effect_metadata_buffer_table_id
×
5989
        .0;
×
5990
    let effect_metadata_offset =
×
5991
        effect_metadata_index as u64 * gpu_limits.effect_metadata_aligned_size.get() as u64;
×
5992
    trace!(
×
5993
        "Draw up to {} particles with {} vertices per particle for batch from buffer #{} \
×
5994
            (effect_metadata_index={}, offset={}B).",
×
5995
        effect_batch.slice.len(),
×
5996
        render_mesh.vertex_count,
×
5997
        effect_batch.buffer_index,
×
5998
        effect_metadata_index,
×
5999
        effect_metadata_offset,
×
6000
    );
6001

6002
    // Note: the indirect draw args are the first few fields of GpuEffectMetadata
6003
    let Some(indirect_buffer) = effects_meta.effect_metadata_buffer.buffer() else {
×
6004
        trace!(
×
6005
            "The metadata buffer containing the indirect draw args is not ready for batch buf=#{}. Skipping draw call.",
×
6006
            effect_batch.buffer_index,
×
6007
        );
6008
        return;
×
6009
    };
6010

6011
    match render_mesh.buffer_info {
×
6012
        RenderMeshBufferInfo::Indexed { index_format, .. } => {
×
6013
            let Some(index_buffer_slice) = mesh_allocator.mesh_index_slice(&effect_batch.mesh)
×
6014
            else {
×
6015
                return;
×
6016
            };
6017

6018
            pass.set_index_buffer(index_buffer_slice.buffer.slice(..), 0, index_format);
×
6019
            pass.draw_indexed_indirect(indirect_buffer, effect_metadata_offset);
×
6020
        }
6021
        RenderMeshBufferInfo::NonIndexed => {
×
6022
            pass.draw_indirect(indirect_buffer, effect_metadata_offset);
×
6023
        }
6024
    }
6025
}
6026

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

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

6071
#[cfg(feature = "3d")]
6072
impl Draw<AlphaMask3d> for DrawEffects {
6073
    fn draw<'w>(
×
6074
        &mut self,
6075
        world: &'w World,
6076
        pass: &mut TrackedRenderPass<'w>,
6077
        view: Entity,
6078
        item: &AlphaMask3d,
6079
    ) -> Result<(), DrawError> {
6080
        trace!("Draw<AlphaMask3d>: view={:?}", view);
×
6081
        draw(
6082
            world,
×
6083
            pass,
×
6084
            view,
×
6085
            item.representative_entity,
×
NEW
6086
            item.batch_set_key.pipeline,
×
6087
            &mut self.params,
×
6088
        );
6089
        Ok(())
×
6090
    }
6091
}
6092

6093
#[cfg(feature = "3d")]
6094
impl Draw<Opaque3d> for DrawEffects {
6095
    fn draw<'w>(
×
6096
        &mut self,
6097
        world: &'w World,
6098
        pass: &mut TrackedRenderPass<'w>,
6099
        view: Entity,
6100
        item: &Opaque3d,
6101
    ) -> Result<(), DrawError> {
6102
        trace!("Draw<Opaque3d>: view={:?}", view);
×
6103
        draw(
6104
            world,
×
6105
            pass,
×
6106
            view,
×
6107
            item.representative_entity,
×
NEW
6108
            item.batch_set_key.pipeline,
×
6109
            &mut self.params,
×
6110
        );
6111
        Ok(())
×
6112
    }
6113
}
6114

6115
/// Render node to run the simulation sub-graph once per frame.
6116
///
6117
/// This node doesn't simulate anything by itself, but instead schedules the
6118
/// simulation sub-graph, where other nodes like [`VfxSimulateNode`] do the
6119
/// actual simulation.
6120
///
6121
/// The simulation sub-graph is scheduled to run before the [`CameraDriverNode`]
6122
/// renders all the views, such that rendered views have access to the
6123
/// just-simulated particles to render them.
6124
///
6125
/// [`CameraDriverNode`]: bevy::render::camera::CameraDriverNode
6126
pub(crate) struct VfxSimulateDriverNode;
6127

6128
impl Node for VfxSimulateDriverNode {
6129
    fn run(
×
6130
        &self,
6131
        graph: &mut RenderGraphContext,
6132
        _render_context: &mut RenderContext,
6133
        _world: &World,
6134
    ) -> Result<(), NodeRunError> {
6135
        graph.run_sub_graph(
×
6136
            crate::plugin::simulate_graph::HanabiSimulateGraph,
×
6137
            vec![],
×
6138
            None,
×
6139
        )?;
6140
        Ok(())
×
6141
    }
6142
}
6143

6144
#[derive(Debug, Clone, PartialEq, Eq)]
6145
enum HanabiPipelineId {
6146
    Invalid,
6147
    Cached(CachedComputePipelineId),
6148
}
6149

6150
pub(crate) enum ComputePipelineError {
6151
    Queued,
6152
    Creating,
6153
    Error,
6154
}
6155

6156
impl From<&CachedPipelineState> for ComputePipelineError {
6157
    fn from(value: &CachedPipelineState) -> Self {
×
6158
        match value {
×
6159
            CachedPipelineState::Queued => Self::Queued,
×
6160
            CachedPipelineState::Creating(_) => Self::Creating,
×
6161
            CachedPipelineState::Err(_) => Self::Error,
×
6162
            _ => panic!("Trying to convert Ok state to error."),
×
6163
        }
6164
    }
6165
}
6166

6167
pub(crate) struct HanabiComputePass<'a> {
6168
    /// Pipeline cache to fetch cached compute pipelines by ID.
6169
    pipeline_cache: &'a PipelineCache,
6170
    /// WGPU compute pass.
6171
    compute_pass: ComputePass<'a>,
6172
    /// Current pipeline (cached).
6173
    pipeline_id: HanabiPipelineId,
6174
}
6175

6176
impl<'a> Deref for HanabiComputePass<'a> {
6177
    type Target = ComputePass<'a>;
6178

6179
    fn deref(&self) -> &Self::Target {
×
6180
        &self.compute_pass
×
6181
    }
6182
}
6183

6184
impl DerefMut for HanabiComputePass<'_> {
6185
    fn deref_mut(&mut self) -> &mut Self::Target {
×
6186
        &mut self.compute_pass
×
6187
    }
6188
}
6189

6190
impl<'a> HanabiComputePass<'a> {
6191
    pub fn new(pipeline_cache: &'a PipelineCache, compute_pass: ComputePass<'a>) -> Self {
×
6192
        Self {
6193
            pipeline_cache,
6194
            compute_pass,
6195
            pipeline_id: HanabiPipelineId::Invalid,
6196
        }
6197
    }
6198

6199
    pub fn set_cached_compute_pipeline(
×
6200
        &mut self,
6201
        pipeline_id: CachedComputePipelineId,
6202
    ) -> Result<(), ComputePipelineError> {
6203
        trace!("set_cached_compute_pipeline() id={pipeline_id:?}");
×
6204
        if HanabiPipelineId::Cached(pipeline_id) == self.pipeline_id {
×
6205
            trace!("-> already set; skipped");
×
6206
            return Ok(());
×
6207
        }
6208
        let Some(pipeline) = self.pipeline_cache.get_compute_pipeline(pipeline_id) else {
×
6209
            let state = self.pipeline_cache.get_compute_pipeline_state(pipeline_id);
×
6210
            if let CachedPipelineState::Err(err) = state {
×
6211
                error!(
×
6212
                    "Failed to find compute pipeline #{}: {:?}",
×
6213
                    pipeline_id.id(),
×
6214
                    err
×
6215
                );
6216
            } else {
6217
                debug!("Compute pipeline not ready #{}", pipeline_id.id());
×
6218
            }
6219
            return Err(state.into());
×
6220
        };
6221
        self.compute_pass.set_pipeline(pipeline);
×
6222
        self.pipeline_id = HanabiPipelineId::Cached(pipeline_id);
×
6223
        Ok(())
×
6224
    }
6225
}
6226

6227
/// Render node to run the simulation of all effects once per frame.
6228
///
6229
/// Runs inside the simulation sub-graph, looping over all extracted effect
6230
/// batches to simulate them.
6231
pub(crate) struct VfxSimulateNode {}
6232

6233
impl VfxSimulateNode {
6234
    /// Create a new node for simulating the effects of the given world.
6235
    pub fn new(_world: &mut World) -> Self {
×
6236
        Self {}
6237
    }
6238

6239
    /// Begin a new compute pass and return a wrapper with extra
6240
    /// functionalities.
6241
    pub fn begin_compute_pass<'encoder>(
×
6242
        &self,
6243
        label: &str,
6244
        pipeline_cache: &'encoder PipelineCache,
6245
        render_context: &'encoder mut RenderContext,
6246
    ) -> HanabiComputePass<'encoder> {
6247
        let compute_pass =
×
6248
            render_context
×
6249
                .command_encoder()
6250
                .begin_compute_pass(&ComputePassDescriptor {
×
6251
                    label: Some(label),
×
6252
                    timestamp_writes: None,
×
6253
                });
6254
        HanabiComputePass::new(pipeline_cache, compute_pass)
×
6255
    }
6256
}
6257

6258
impl Node for VfxSimulateNode {
6259
    fn input(&self) -> Vec<SlotInfo> {
×
6260
        vec![]
×
6261
    }
6262

6263
    fn update(&mut self, _world: &mut World) {}
×
6264

6265
    fn run(
×
6266
        &self,
6267
        _graph: &mut RenderGraphContext,
6268
        render_context: &mut RenderContext,
6269
        world: &World,
6270
    ) -> Result<(), NodeRunError> {
6271
        trace!("VfxSimulateNode::run()");
×
6272

6273
        let pipeline_cache = world.resource::<PipelineCache>();
×
6274
        let effects_meta = world.resource::<EffectsMeta>();
×
6275
        let effect_bind_groups = world.resource::<EffectBindGroups>();
×
6276
        let property_bind_groups = world.resource::<PropertyBindGroups>();
×
6277
        let sort_bind_groups = world.resource::<SortBindGroups>();
×
6278
        let utils_pipeline = world.resource::<UtilsPipeline>();
×
6279
        let effect_cache = world.resource::<EffectCache>();
×
6280
        let event_cache = world.resource::<EventCache>();
×
6281
        let gpu_buffer_operations = world.resource::<GpuBufferOperations>();
×
6282
        let sorted_effect_batches = world.resource::<SortedEffectBatches>();
×
6283
        let init_fill_dispatch_queue = world.resource::<InitFillDispatchQueue>();
×
6284

6285
        // Make sure to schedule any buffer copy before accessing their content later in
6286
        // the GPU commands below.
6287
        {
6288
            let command_encoder = render_context.command_encoder();
×
6289
            effects_meta
×
6290
                .update_dispatch_indirect_buffer
×
6291
                .write_buffers(command_encoder);
×
6292
            effects_meta
×
6293
                .effect_metadata_buffer
×
6294
                .write_buffer(command_encoder);
×
6295
            event_cache.write_buffers(command_encoder);
×
6296
            sort_bind_groups.write_buffers(command_encoder);
×
6297
        }
6298

6299
        // Compute init fill dispatch pass - Fill the indirect dispatch structs for any
6300
        // upcoming init pass of this frame, based on the GPU spawn events emitted by
6301
        // the update pass of their parent effect during the previous frame.
6302
        if let Some(queue_index) = init_fill_dispatch_queue.submitted_queue_index.as_ref() {
×
6303
            gpu_buffer_operations.dispatch(
6304
                *queue_index,
6305
                render_context,
6306
                utils_pipeline,
6307
                Some("hanabi:init_indirect_fill_dispatch"),
6308
            );
6309
        }
6310

6311
        // If there's no batch, there's nothing more to do. Avoid continuing because
6312
        // some GPU resources are missing, which is expected when there's no effect but
6313
        // is an error (and will log warnings/errors) otherwise.
6314
        if sorted_effect_batches.is_empty() {
×
6315
            return Ok(());
×
6316
        }
6317

6318
        // Compute init pass
6319
        {
6320
            trace!("init: loop over effect batches...");
×
6321

6322
            let mut compute_pass =
6323
                self.begin_compute_pass("hanabi:init", pipeline_cache, render_context);
6324

6325
            // Bind group simparams@0 is common to everything, only set once per init pass
6326
            compute_pass.set_bind_group(
6327
                0,
6328
                effects_meta
6329
                    .indirect_sim_params_bind_group
6330
                    .as_ref()
6331
                    .unwrap(),
6332
                &[],
6333
            );
6334

6335
            // Dispatch init compute jobs for all batches
6336
            for effect_batch in sorted_effect_batches.iter() {
×
6337
                // Do not dispatch any init work if there's nothing to spawn this frame for the
6338
                // batch. Note that this hopefully should have been skipped earlier.
6339
                {
6340
                    let use_indirect_dispatch = effect_batch
×
6341
                        .layout_flags
×
6342
                        .contains(LayoutFlags::CONSUME_GPU_SPAWN_EVENTS);
×
6343
                    match effect_batch.spawn_info {
×
6344
                        BatchSpawnInfo::CpuSpawner { total_spawn_count } => {
×
6345
                            assert!(!use_indirect_dispatch);
×
6346
                            if total_spawn_count == 0 {
×
6347
                                continue;
×
6348
                            }
6349
                        }
6350
                        BatchSpawnInfo::GpuSpawner { .. } => {
6351
                            assert!(use_indirect_dispatch);
×
6352
                        }
6353
                    }
6354
                }
6355

6356
                // Fetch bind group particle@1
6357
                let Some(particle_bind_group) =
×
6358
                    effect_cache.particle_sim_bind_group(effect_batch.buffer_index)
×
6359
                else {
6360
                    error!(
×
6361
                        "Failed to find init particle@1 bind group for buffer index {}",
×
6362
                        effect_batch.buffer_index
6363
                    );
6364
                    continue;
×
6365
                };
6366

6367
                // Fetch bind group metadata@3
6368
                let Some(metadata_bind_group) = effect_bind_groups
×
6369
                    .init_metadata_bind_groups
6370
                    .get(&effect_batch.buffer_index)
6371
                else {
6372
                    error!(
×
6373
                        "Failed to find init metadata@3 bind group for buffer index {}",
×
6374
                        effect_batch.buffer_index
6375
                    );
6376
                    continue;
×
6377
                };
6378

6379
                if compute_pass
6380
                    .set_cached_compute_pipeline(effect_batch.init_and_update_pipeline_ids.init)
6381
                    .is_err()
6382
                {
6383
                    continue;
×
6384
                }
6385

6386
                // Compute dynamic offsets
6387
                let spawner_base = effect_batch.spawner_base;
×
6388
                let spawner_aligned_size = effects_meta.spawner_buffer.aligned_size();
×
6389
                debug_assert!(spawner_aligned_size >= GpuSpawnerParams::min_size().get() as usize);
×
6390
                let spawner_offset = spawner_base * spawner_aligned_size as u32;
×
6391
                let property_offset = effect_batch.property_offset;
×
6392

6393
                // Setup init pass
6394
                compute_pass.set_bind_group(1, particle_bind_group, &[]);
×
6395
                let offsets = if let Some(property_offset) = property_offset {
×
6396
                    vec![spawner_offset, property_offset]
6397
                } else {
6398
                    vec![spawner_offset]
×
6399
                };
6400
                compute_pass.set_bind_group(
×
6401
                    2,
6402
                    property_bind_groups
×
6403
                        .get(effect_batch.property_key.as_ref())
×
6404
                        .unwrap(),
×
6405
                    &offsets[..],
×
6406
                );
6407
                compute_pass.set_bind_group(3, &metadata_bind_group.bind_group, &[]);
×
6408

6409
                // Dispatch init job
6410
                match effect_batch.spawn_info {
×
6411
                    // Indirect dispatch via GPU spawn events
6412
                    BatchSpawnInfo::GpuSpawner {
6413
                        init_indirect_dispatch_index,
×
6414
                        ..
×
6415
                    } => {
×
6416
                        assert!(effect_batch
×
6417
                            .layout_flags
×
6418
                            .contains(LayoutFlags::CONSUME_GPU_SPAWN_EVENTS));
×
6419

6420
                        // Note: the indirect offset of a dispatch workgroup only needs
6421
                        // 4-byte alignment
6422
                        assert_eq!(GpuDispatchIndirect::min_size().get(), 12);
×
6423
                        let indirect_offset = init_indirect_dispatch_index as u64 * 12;
×
6424

6425
                        trace!(
×
6426
                            "record commands for indirect init pipeline of effect {:?} \
×
6427
                                init_indirect_dispatch_index={} \
×
6428
                                indirect_offset={} \
×
6429
                                spawner_base={} \
×
6430
                                spawner_offset={} \
×
6431
                                property_key={:?}...",
×
6432
                            effect_batch.handle,
6433
                            init_indirect_dispatch_index,
6434
                            indirect_offset,
6435
                            spawner_base,
6436
                            spawner_offset,
6437
                            effect_batch.property_key,
6438
                        );
6439

6440
                        compute_pass.dispatch_workgroups_indirect(
×
6441
                            event_cache.init_indirect_dispatch_buffer().unwrap(),
×
6442
                            indirect_offset,
×
6443
                        );
6444
                    }
6445

6446
                    // Direct dispatch via CPU spawn count
6447
                    BatchSpawnInfo::CpuSpawner {
6448
                        total_spawn_count: spawn_count,
×
6449
                    } => {
×
6450
                        assert!(!effect_batch
×
6451
                            .layout_flags
×
6452
                            .contains(LayoutFlags::CONSUME_GPU_SPAWN_EVENTS));
×
6453

6454
                        const WORKGROUP_SIZE: u32 = 64;
6455
                        let workgroup_count = spawn_count.div_ceil(WORKGROUP_SIZE);
×
6456

6457
                        trace!(
×
6458
                            "record commands for init pipeline of effect {:?} \
×
6459
                                (spawn {} particles => {} workgroups) spawner_base={} \
×
6460
                                spawner_offset={} \
×
6461
                                property_key={:?}...",
×
6462
                            effect_batch.handle,
6463
                            spawn_count,
6464
                            workgroup_count,
6465
                            spawner_base,
6466
                            spawner_offset,
6467
                            effect_batch.property_key,
6468
                        );
6469

6470
                        compute_pass.dispatch_workgroups(workgroup_count, 1, 1);
6471
                    }
6472
                }
6473

6474
                trace!("init compute dispatched");
×
6475
            }
6476
        }
6477

6478
        // Compute indirect dispatch pass
6479
        if effects_meta.spawner_buffer.buffer().is_some()
×
6480
            && !effects_meta.spawner_buffer.is_empty()
×
6481
            && effects_meta.indirect_metadata_bind_group.is_some()
×
6482
            && effects_meta.indirect_sim_params_bind_group.is_some()
×
6483
        {
6484
            // Only start a compute pass if there's an effect; makes things clearer in
6485
            // debugger.
6486
            let mut compute_pass =
×
6487
                self.begin_compute_pass("hanabi:indirect_dispatch", pipeline_cache, render_context);
×
6488

6489
            // Dispatch indirect dispatch compute job
6490
            trace!("record commands for indirect dispatch pipeline...");
×
6491

6492
            let has_gpu_spawn_events = !event_cache.child_infos().is_empty();
×
6493
            if has_gpu_spawn_events {
×
6494
                if let Some(indirect_child_info_buffer_bind_group) =
×
6495
                    event_cache.indirect_child_info_buffer_bind_group()
×
6496
                {
6497
                    assert!(has_gpu_spawn_events);
6498
                    compute_pass.set_bind_group(3, indirect_child_info_buffer_bind_group, &[]);
×
6499
                } else {
6500
                    error!("Missing child_info_buffer@3 bind group for the vfx_indirect pass.");
×
6501
                    // render_context
6502
                    //     .command_encoder()
6503
                    //     .insert_debug_marker("ERROR:MissingIndirectBindGroup3");
6504
                    // FIXME - Bevy doesn't allow returning custom errors here...
6505
                    return Ok(());
×
6506
                }
6507
            }
6508

6509
            if compute_pass
×
6510
                .set_cached_compute_pipeline(effects_meta.active_indirect_pipeline_id)
×
6511
                .is_err()
6512
            {
6513
                // FIXME - Bevy doesn't allow returning custom errors here...
6514
                return Ok(());
×
6515
            }
6516

6517
            //error!("FIXME - effect_metadata_buffer has gaps!!!! this won't work. len() is
6518
            // the size exluding gaps!");
6519
            const WORKGROUP_SIZE: u32 = 64;
6520
            //let total_effect_count = effects_meta.effect_metadata_buffer.len();
6521
            let total_effect_count = effects_meta.spawner_buffer.len() as u32;
6522
            let workgroup_count = total_effect_count.div_ceil(WORKGROUP_SIZE);
6523

6524
            // Setup vfx_indirect pass
6525
            compute_pass.set_bind_group(
6526
                0,
6527
                effects_meta
6528
                    .indirect_sim_params_bind_group
6529
                    .as_ref()
6530
                    .unwrap(),
6531
                &[],
6532
            );
6533
            compute_pass.set_bind_group(
6534
                1,
6535
                // FIXME - got some unwrap() panic here, investigate... possibly race
6536
                // condition!
6537
                effects_meta.indirect_metadata_bind_group.as_ref().unwrap(),
6538
                &[],
6539
            );
6540
            compute_pass.set_bind_group(
6541
                2,
6542
                effects_meta.indirect_spawner_bind_group.as_ref().unwrap(),
6543
                &[],
6544
            );
6545
            compute_pass.dispatch_workgroups(workgroup_count, 1, 1);
6546
            trace!(
6547
                "indirect dispatch compute dispatched: total_effect_count={} workgroup_count={}",
×
6548
                total_effect_count,
6549
                workgroup_count
6550
            );
6551
        }
6552

6553
        // Compute update pass
6554
        {
6555
            let Some(indirect_buffer) = effects_meta.update_dispatch_indirect_buffer.buffer()
×
6556
            else {
6557
                warn!("Missing indirect buffer for update pass, cannot dispatch anything.");
×
6558
                render_context
×
6559
                    .command_encoder()
6560
                    .insert_debug_marker("ERROR:MissingUpdateIndirectBuffer");
6561
                // FIXME - Bevy doesn't allow returning custom errors here...
6562
                return Ok(());
×
6563
            };
6564

6565
            let mut compute_pass =
6566
                self.begin_compute_pass("hanabi:update", pipeline_cache, render_context);
6567

6568
            // Bind group simparams@0 is common to everything, only set once per update pass
6569
            compute_pass.set_bind_group(
6570
                0,
6571
                effects_meta
6572
                    .indirect_sim_params_bind_group
6573
                    .as_ref()
6574
                    .unwrap(),
6575
                &[],
6576
            );
6577

6578
            // Dispatch update compute jobs
6579
            for effect_batch in sorted_effect_batches.iter() {
×
6580
                // Fetch bind group particle@1
6581
                let Some(particle_bind_group) =
×
6582
                    effect_cache.particle_sim_bind_group(effect_batch.buffer_index)
×
6583
                else {
6584
                    error!(
×
6585
                        "Failed to find update particle@1 bind group for buffer index {}",
×
6586
                        effect_batch.buffer_index
6587
                    );
6588
                    continue;
×
6589
                };
6590

6591
                // Fetch bind group metadata@3
6592
                let Some(metadata_bind_group) = effect_bind_groups
×
6593
                    .update_metadata_bind_groups
6594
                    .get(&effect_batch.buffer_index)
6595
                else {
6596
                    error!(
×
6597
                        "Failed to find update metadata@3 bind group for buffer index {}",
×
6598
                        effect_batch.buffer_index
6599
                    );
6600
                    continue;
×
6601
                };
6602

6603
                // Fetch compute pipeline
6604
                if compute_pass
6605
                    .set_cached_compute_pipeline(effect_batch.init_and_update_pipeline_ids.update)
6606
                    .is_err()
6607
                {
6608
                    continue;
×
6609
                }
6610

6611
                // Compute dynamic offsets
6612
                let spawner_index = effect_batch.spawner_base;
×
6613
                let spawner_aligned_size = effects_meta.spawner_buffer.aligned_size();
×
6614
                assert!(spawner_aligned_size >= GpuSpawnerParams::min_size().get() as usize);
×
6615
                let spawner_offset = spawner_index * spawner_aligned_size as u32;
×
6616
                let property_offset = effect_batch.property_offset;
×
6617

6618
                trace!(
×
6619
                    "record commands for update pipeline of effect {:?} spawner_base={}",
×
6620
                    effect_batch.handle,
6621
                    spawner_index,
6622
                );
6623

6624
                // Setup update pass
6625
                compute_pass.set_bind_group(1, particle_bind_group, &[]);
6626
                let offsets = if let Some(property_offset) = property_offset {
×
6627
                    vec![spawner_offset, property_offset]
6628
                } else {
6629
                    vec![spawner_offset]
×
6630
                };
6631
                compute_pass.set_bind_group(
6632
                    2,
6633
                    property_bind_groups
6634
                        .get(effect_batch.property_key.as_ref())
6635
                        .unwrap(),
6636
                    &offsets[..],
6637
                );
6638
                compute_pass.set_bind_group(3, &metadata_bind_group.bind_group, &[]);
6639

6640
                // Dispatch update job
6641
                let dispatch_indirect_offset = effect_batch
6642
                    .dispatch_buffer_indices
6643
                    .update_dispatch_indirect_buffer_row_index
6644
                    * 12;
6645
                trace!(
6646
                    "dispatch_workgroups_indirect: buffer={:?} offset={}B",
×
6647
                    indirect_buffer,
6648
                    dispatch_indirect_offset,
6649
                );
6650
                compute_pass
6651
                    .dispatch_workgroups_indirect(indirect_buffer, dispatch_indirect_offset as u64);
6652

6653
                trace!("update compute dispatched");
×
6654
            }
6655
        }
6656

6657
        // Compute sort fill dispatch pass - Fill the indirect dispatch structs for any
6658
        // batch of particles which needs sorting, based on the actual number of alive
6659
        // particles in the batch after their update in the compute update pass. Since
6660
        // particles may die during update, this may be different from the number of
6661
        // particles updated.
6662
        if let Some(queue_index) = sorted_effect_batches.dispatch_queue_index.as_ref() {
×
6663
            gpu_buffer_operations.dispatch(
6664
                *queue_index,
6665
                render_context,
6666
                utils_pipeline,
6667
                Some("hanabi:sort_fill_dispatch"),
6668
            );
6669
        }
6670

6671
        // Compute sort pass
6672
        {
6673
            let mut compute_pass =
6674
                self.begin_compute_pass("hanabi:sort", pipeline_cache, render_context);
6675

6676
            let effect_metadata_buffer = effects_meta.effect_metadata_buffer.buffer().unwrap();
6677
            let indirect_buffer = sort_bind_groups.indirect_buffer().unwrap();
6678

6679
            // Loop on batches and find those which need sorting
6680
            for effect_batch in sorted_effect_batches.iter() {
×
6681
                trace!("Processing effect batch for sorting...");
×
6682
                if !effect_batch.layout_flags.contains(LayoutFlags::RIBBONS) {
×
6683
                    continue;
×
6684
                }
6685
                assert!(effect_batch.particle_layout.contains(Attribute::RIBBON_ID));
×
6686
                assert!(effect_batch.particle_layout.contains(Attribute::AGE)); // or is that optional?
×
6687

6688
                let Some(effect_buffer) = effect_cache.get_buffer(effect_batch.buffer_index) else {
×
6689
                    warn!("Missing sort-fill effect buffer.");
×
6690
                    continue;
×
6691
                };
6692

6693
                let indirect_dispatch_index = *effect_batch
6694
                    .sort_fill_indirect_dispatch_index
6695
                    .as_ref()
6696
                    .unwrap();
6697
                let indirect_offset =
6698
                    sort_bind_groups.get_indirect_dispatch_byte_offset(indirect_dispatch_index);
6699

6700
                // Fill the sort buffer with the key-value pairs to sort
6701
                {
6702
                    compute_pass.push_debug_group("hanabi:sort_fill");
6703

6704
                    // Fetch compute pipeline
6705
                    let Some(pipeline_id) =
×
6706
                        sort_bind_groups.get_sort_fill_pipeline_id(&effect_batch.particle_layout)
6707
                    else {
6708
                        warn!("Missing sort-fill pipeline.");
×
6709
                        continue;
×
6710
                    };
6711
                    if compute_pass
6712
                        .set_cached_compute_pipeline(pipeline_id)
6713
                        .is_err()
6714
                    {
6715
                        compute_pass.pop_debug_group();
×
6716
                        // FIXME - Bevy doesn't allow returning custom errors here...
6717
                        return Ok(());
×
6718
                    }
6719

6720
                    // Bind group sort_fill@0
6721
                    let particle_buffer = effect_buffer.particle_buffer();
×
6722
                    let indirect_index_buffer = effect_buffer.indirect_index_buffer();
×
6723
                    let Some(bind_group) = sort_bind_groups.sort_fill_bind_group(
×
6724
                        particle_buffer.id(),
6725
                        indirect_index_buffer.id(),
6726
                        effect_metadata_buffer.id(),
6727
                    ) else {
6728
                        warn!("Missing sort-fill bind group.");
×
6729
                        continue;
×
6730
                    };
6731
                    let particle_offset = effect_buffer.particle_offset(effect_batch.slice.start);
6732
                    let indirect_index_offset =
6733
                        effect_buffer.indirect_index_offset(effect_batch.slice.start);
6734
                    let effect_metadata_offset = effects_meta.gpu_limits.effect_metadata_offset(
6735
                        effect_batch
6736
                            .dispatch_buffer_indices
6737
                            .effect_metadata_buffer_table_id
6738
                            .0,
6739
                    ) as u32;
6740
                    compute_pass.set_bind_group(
6741
                        0,
6742
                        bind_group,
6743
                        &[
6744
                            particle_offset,
6745
                            indirect_index_offset,
6746
                            effect_metadata_offset,
6747
                        ],
6748
                    );
6749

6750
                    compute_pass
6751
                        .dispatch_workgroups_indirect(indirect_buffer, indirect_offset as u64);
6752
                    trace!("Dispatched sort-fill with indirect offset +{indirect_offset}");
×
6753

6754
                    compute_pass.pop_debug_group();
6755
                }
6756

6757
                // Do the actual sort
6758
                {
6759
                    compute_pass.push_debug_group("hanabi:sort");
6760

6761
                    if compute_pass
6762
                        .set_cached_compute_pipeline(sort_bind_groups.sort_pipeline_id())
6763
                        .is_err()
6764
                    {
6765
                        compute_pass.pop_debug_group();
×
6766
                        // FIXME - Bevy doesn't allow returning custom errors here...
6767
                        return Ok(());
×
6768
                    }
6769

6770
                    compute_pass.set_bind_group(0, sort_bind_groups.sort_bind_group(), &[]);
×
6771
                    compute_pass
×
6772
                        .dispatch_workgroups_indirect(indirect_buffer, indirect_offset as u64);
×
6773
                    trace!("Dispatched sort with indirect offset +{indirect_offset}");
×
6774

6775
                    compute_pass.pop_debug_group();
6776
                }
6777

6778
                // Copy the sorted particle indices back into the indirect index buffer, where
6779
                // the render pass will read them.
6780
                {
6781
                    compute_pass.push_debug_group("hanabi:copy_sorted_indices");
6782

6783
                    // Fetch compute pipeline
6784
                    let pipeline_id = sort_bind_groups.get_sort_copy_pipeline_id();
6785
                    if compute_pass
6786
                        .set_cached_compute_pipeline(pipeline_id)
6787
                        .is_err()
6788
                    {
6789
                        compute_pass.pop_debug_group();
×
6790
                        // FIXME - Bevy doesn't allow returning custom errors here...
6791
                        return Ok(());
×
6792
                    }
6793

6794
                    // Bind group sort_copy@0
6795
                    let indirect_index_buffer = effect_buffer.indirect_index_buffer();
×
6796
                    let Some(bind_group) = sort_bind_groups.sort_copy_bind_group(
×
6797
                        indirect_index_buffer.id(),
6798
                        effect_metadata_buffer.id(),
6799
                    ) else {
6800
                        warn!("Missing sort-copy bind group.");
×
6801
                        continue;
×
6802
                    };
6803
                    let indirect_index_offset = effect_batch.slice.start;
6804
                    let effect_metadata_offset =
6805
                        effects_meta.effect_metadata_buffer.dynamic_offset(
6806
                            effect_batch
6807
                                .dispatch_buffer_indices
6808
                                .effect_metadata_buffer_table_id,
6809
                        );
6810
                    compute_pass.set_bind_group(
6811
                        0,
6812
                        bind_group,
6813
                        &[indirect_index_offset, effect_metadata_offset],
6814
                    );
6815

6816
                    compute_pass
6817
                        .dispatch_workgroups_indirect(indirect_buffer, indirect_offset as u64);
6818
                    trace!("Dispatched sort-copy with indirect offset +{indirect_offset}");
×
6819

6820
                    compute_pass.pop_debug_group();
6821
                }
6822
            }
6823
        }
6824

6825
        Ok(())
×
6826
    }
6827
}
6828

6829
impl From<LayoutFlags> for ParticleRenderAlphaMaskPipelineKey {
6830
    fn from(layout_flags: LayoutFlags) -> Self {
×
6831
        if layout_flags.contains(LayoutFlags::USE_ALPHA_MASK) {
×
6832
            ParticleRenderAlphaMaskPipelineKey::AlphaMask
×
6833
        } else if layout_flags.contains(LayoutFlags::OPAQUE) {
×
6834
            ParticleRenderAlphaMaskPipelineKey::Opaque
×
6835
        } else {
6836
            ParticleRenderAlphaMaskPipelineKey::Blend
×
6837
        }
6838
    }
6839
}
6840

6841
#[cfg(test)]
6842
mod tests {
6843
    use super::*;
6844

6845
    #[test]
6846
    fn layout_flags() {
6847
        let flags = LayoutFlags::default();
6848
        assert_eq!(flags, LayoutFlags::NONE);
6849
    }
6850

6851
    #[cfg(feature = "gpu_tests")]
6852
    #[test]
6853
    fn gpu_limits() {
6854
        use crate::test_utils::MockRenderer;
6855

6856
        let renderer = MockRenderer::new();
6857
        let device = renderer.device();
6858
        let limits = GpuLimits::from_device(&device);
6859

6860
        // assert!(limits.storage_buffer_align().get() >= 1);
6861
        assert!(limits.effect_metadata_offset(256) >= 256 * GpuEffectMetadata::min_size().get());
6862
    }
6863

6864
    #[cfg(feature = "gpu_tests")]
6865
    #[test]
6866
    fn gpu_ops_ifda() {
6867
        use crate::test_utils::MockRenderer;
6868

6869
        let renderer = MockRenderer::new();
6870
        let device = renderer.device();
6871
        let render_queue = renderer.queue();
6872

6873
        let mut world = World::new();
6874
        world.insert_resource(device.clone());
6875
        let mut buffer_ops = GpuBufferOperations::from_world(&mut world);
6876

6877
        let src_buffer = device.create_buffer(&BufferDescriptor {
6878
            label: None,
6879
            size: 256,
6880
            usage: BufferUsages::STORAGE,
6881
            mapped_at_creation: false,
6882
        });
6883
        let dst_buffer = device.create_buffer(&BufferDescriptor {
6884
            label: None,
6885
            size: 256,
6886
            usage: BufferUsages::STORAGE,
6887
            mapped_at_creation: false,
6888
        });
6889

6890
        // Two consecutive ops can be merged. This includes having contiguous slices
6891
        // both in source and destination.
6892
        buffer_ops.begin_frame();
6893
        {
6894
            let mut q = InitFillDispatchQueue::default();
6895
            q.enqueue(0, 0);
6896
            assert_eq!(q.queue.len(), 1);
6897
            q.enqueue(1, 1);
6898
            // Ops are not batched yet
6899
            assert_eq!(q.queue.len(), 2);
6900
            // On submit, the ops get batched together
6901
            q.submit(&src_buffer, &dst_buffer, &mut buffer_ops);
6902
            assert_eq!(buffer_ops.args_buffer.len(), 1);
6903
        }
6904
        buffer_ops.end_frame(&device, &render_queue);
6905

6906
        // Even if out of order, the init fill dispatch ops are batchable. Here the
6907
        // offsets are enqueued inverted.
6908
        buffer_ops.begin_frame();
6909
        {
6910
            let mut q = InitFillDispatchQueue::default();
6911
            q.enqueue(1, 1);
6912
            assert_eq!(q.queue.len(), 1);
6913
            q.enqueue(0, 0);
6914
            // Ops are not batched yet
6915
            assert_eq!(q.queue.len(), 2);
6916
            // On submit, the ops get batched together
6917
            q.submit(&src_buffer, &dst_buffer, &mut buffer_ops);
6918
            assert_eq!(buffer_ops.args_buffer.len(), 1);
6919
        }
6920
        buffer_ops.end_frame(&device, &render_queue);
6921

6922
        // However, both the source and destination need to be contiguous at the same
6923
        // time. Here they are mixed so we can't batch.
6924
        buffer_ops.begin_frame();
6925
        {
6926
            let mut q = InitFillDispatchQueue::default();
6927
            q.enqueue(0, 1);
6928
            assert_eq!(q.queue.len(), 1);
6929
            q.enqueue(1, 0);
6930
            // Ops are not batched yet
6931
            assert_eq!(q.queue.len(), 2);
6932
            // On submit, the ops cannot get batched together
6933
            q.submit(&src_buffer, &dst_buffer, &mut buffer_ops);
6934
            assert_eq!(buffer_ops.args_buffer.len(), 2);
6935
        }
6936
        buffer_ops.end_frame(&device, &render_queue);
6937
    }
6938
}
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