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

djeedai / bevy_hanabi / 11606205377

31 Oct 2024 04:39AM UTC coverage: 57.93% (+0.07%) from 57.86%
11606205377

push

github

web-flow
Add `wgpu` as core dependency (#395)

Move `wgpu` from a dev-dependency to a core crate dependency, to allow the
direct use of its types.

Clean-up various documentations, and other minor code changes.

15 of 27 new or added lines in 2 files covered. (55.56%)

3 existing lines in 1 file now uncovered.

3543 of 6116 relevant lines covered (57.93%)

23.01 hits per line

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

23.67
/src/render/mod.rs
1
use std::{
2
    borrow::Cow,
3
    num::{NonZero, NonZeroU32, NonZeroU64},
4
    ops::Deref,
5
};
6
use std::{iter, marker::PhantomData};
7

8
use batch::InitAndUpdatePipelineIds;
9
#[cfg(feature = "2d")]
10
use bevy::core_pipeline::core_2d::Transparent2d;
11
#[cfg(feature = "2d")]
12
use bevy::math::FloatOrd;
13
#[cfg(feature = "3d")]
14
use bevy::{
15
    core_pipeline::{
16
        core_3d::{AlphaMask3d, Transparent3d},
17
        prepass::OpaqueNoLightmap3dBinKey,
18
    },
19
    render::render_phase::{BinnedPhaseItem, ViewBinnedRenderPhases},
20
};
21
use bevy::{
22
    ecs::{
23
        prelude::*,
24
        system::{lifetimeless::*, SystemParam, SystemState},
25
    },
26
    log::trace,
27
    prelude::*,
28
    render::{
29
        render_asset::RenderAssets,
30
        render_graph::{Node, NodeRunError, RenderGraphContext, SlotInfo},
31
        render_phase::{
32
            Draw, DrawFunctions, PhaseItemExtraIndex, SortedPhaseItem, TrackedRenderPass,
33
            ViewSortedRenderPhases,
34
        },
35
        render_resource::*,
36
        renderer::{RenderContext, RenderDevice, RenderQueue},
37
        texture::{BevyDefault, GpuImage},
38
        view::{
39
            ExtractedView, ViewTarget, ViewUniform, ViewUniformOffset, ViewUniforms,
40
            VisibleEntities,
41
        },
42
        Extract,
43
    },
44
    utils::HashMap,
45
};
46
use bitflags::bitflags;
47
use bytemuck::{Pod, Zeroable};
48
use effect_cache::TrailDispatchBufferIndices;
49
use fixedbitset::FixedBitSet;
50
use naga_oil::compose::{Composer, NagaModuleDescriptor};
51
use rand::random;
52

53
use crate::{
54
    asset::EffectAsset,
55
    next_multiple_of,
56
    plugin::WithCompiledParticleEffect,
57
    render::{
58
        batch::{BatchesInput, EffectDrawBatch},
59
        effect_cache::DispatchBufferIndices,
60
    },
61
    spawn::{EffectCloner, EffectInitializer, EffectInitializers, Initializer},
62
    AlphaMode, Attribute, CompiledParticleEffect, EffectProperties, EffectShader, EffectSimulation,
63
    HanabiPlugin, ParticleLayout, PropertyLayout, RemovedEffectsEvent, SimulationCondition,
64
    TextureLayout, ToWgslString,
65
};
66

67
mod aligned_buffer_vec;
68
mod batch;
69
mod buffer_table;
70
mod effect_cache;
71
mod shader_cache;
72

73
use aligned_buffer_vec::AlignedBufferVec;
74
use buffer_table::{BufferTable, BufferTableId};
75
pub(crate) use effect_cache::{EffectCache, EffectCacheId};
76
pub use shader_cache::ShaderCache;
77

78
use self::batch::EffectBatches;
79

80
// Size of an indirect index (including both parts of the ping-pong buffer) in
81
// bytes.
82
const INDIRECT_INDEX_SIZE: u32 = 12;
83

84
/// Simulation parameters, available to all shaders of all effects.
85
#[derive(Debug, Default, Clone, Copy, Resource)]
86
pub(crate) struct SimParams {
87
    /// Current effect system simulation time since startup, in seconds.
88
    /// This is based on the [`Time<EffectSimulation>`](EffectSimulation) clock.
89
    time: f64,
90
    /// Delta time, in seconds, since last effect system update.
91
    delta_time: f32,
92

93
    /// Current virtual time since startup, in seconds.
94
    /// This is based on the [`Time<Virtual>`](Virtual) clock.
95
    virtual_time: f64,
96
    /// Virtual delta time, in seconds, since last effect system update.
97
    virtual_delta_time: f32,
98

99
    /// Current real time since startup, in seconds.
100
    /// This is based on the [`Time<Real>`](Real) clock.
101
    real_time: f64,
102
    /// Real delta time, in seconds, since last effect system update.
103
    real_delta_time: f32,
104
}
105

106
/// GPU representation of [`SimParams`], as well as additional per-frame
107
/// effect-independent values.
108
#[repr(C)]
109
#[derive(Debug, Copy, Clone, Pod, Zeroable, ShaderType)]
110
struct GpuSimParams {
111
    /// Delta time, in seconds, since last effect system update.
112
    delta_time: f32,
113
    /// Current effect system simulation time since startup, in seconds.
114
    ///
115
    /// This is a lower-precision variant of [`SimParams::time`].
116
    time: f32,
117
    /// Virtual delta time, in seconds, since last effect system update.
118
    virtual_delta_time: f32,
119
    /// Current virtual time since startup, in seconds.
120
    ///
121
    /// This is a lower-precision variant of [`SimParams::time`].
122
    virtual_time: f32,
123
    /// Real delta time, in seconds, since last effect system update.
124
    real_delta_time: f32,
125
    /// Current real time since startup, in seconds.
126
    ///
127
    /// This is a lower-precision variant of [`SimParams::time`].
128
    real_time: f32,
129
    /// Total number of groups to simulate this frame. Used by the indirect
130
    /// compute pipeline to cap the compute thread to the actual number of
131
    /// groups to process.
132
    ///
133
    /// This is only used by the `vfx_indirect` compute shader.
134
    num_groups: u32,
135
}
136

137
impl Default for GpuSimParams {
138
    fn default() -> Self {
11✔
139
        Self {
140
            delta_time: 0.04,
141
            time: 0.0,
142
            virtual_delta_time: 0.04,
143
            virtual_time: 0.0,
144
            real_delta_time: 0.04,
145
            real_time: 0.0,
146
            num_groups: 0,
147
        }
148
    }
149
}
150

151
impl From<SimParams> for GpuSimParams {
152
    #[inline]
UNCOV
153
    fn from(src: SimParams) -> Self {
×
NEW
154
        Self::from(&src)
×
155
    }
156
}
157

158
impl From<&SimParams> for GpuSimParams {
159
    fn from(src: &SimParams) -> Self {
10✔
160
        Self {
161
            delta_time: src.delta_time,
10✔
162
            time: src.time as f32,
10✔
163
            virtual_delta_time: src.virtual_delta_time,
10✔
164
            virtual_time: src.virtual_time as f32,
10✔
165
            real_delta_time: src.real_delta_time,
10✔
166
            real_time: src.real_time as f32,
10✔
167
            ..default()
168
        }
169
    }
170
}
171

172
/// Compressed representation of a transform for GPU transfer.
173
///
174
/// The transform is stored as the three first rows of a transposed [`Mat4`],
175
/// assuming the last row is the unit row [`Vec4::W`]. The transposing ensures
176
/// that the three values are [`Vec4`] types which are naturally aligned and
177
/// without padding when used in WGSL. Without this, storing only the first
178
/// three components of each column would introduce padding, and would use the
179
/// same storage size on GPU as a full [`Mat4`].
180
#[repr(C)]
181
#[derive(Debug, Default, Clone, Copy, Pod, Zeroable, ShaderType)]
182
pub(crate) struct GpuCompressedTransform {
183
    pub x_row: Vec4,
184
    pub y_row: Vec4,
185
    pub z_row: Vec4,
186
}
187

188
impl From<Mat4> for GpuCompressedTransform {
189
    fn from(value: Mat4) -> Self {
×
190
        let tr = value.transpose();
×
191
        #[cfg(test)]
192
        crate::test_utils::assert_approx_eq!(tr.w_axis, Vec4::W);
193
        Self {
194
            x_row: tr.x_axis,
×
195
            y_row: tr.y_axis,
×
196
            z_row: tr.z_axis,
×
197
        }
198
    }
199
}
200

201
impl From<&Mat4> for GpuCompressedTransform {
202
    fn from(value: &Mat4) -> Self {
×
203
        let tr = value.transpose();
×
204
        #[cfg(test)]
205
        crate::test_utils::assert_approx_eq!(tr.w_axis, Vec4::W);
206
        Self {
207
            x_row: tr.x_axis,
×
208
            y_row: tr.y_axis,
×
209
            z_row: tr.z_axis,
×
210
        }
211
    }
212
}
213

214
impl GpuCompressedTransform {
215
    /// Returns the translation as represented by this transform.
216
    #[allow(dead_code)]
217
    pub fn translation(&self) -> Vec3 {
×
218
        Vec3 {
219
            x: self.x_row.w,
×
220
            y: self.y_row.w,
×
221
            z: self.z_row.w,
×
222
        }
223
    }
224
}
225

226
/// Extension trait for shader types stored in a WGSL storage buffer.
227
pub(crate) trait StorageType {
228
    /// Get the aligned size, in bytes, of this type such that it aligns to the
229
    /// given alignment, in bytes.
230
    ///
231
    /// This is mainly used to align GPU types to device requirements.
232
    fn aligned_size(alignment: u32) -> NonZeroU64;
233

234
    /// Get the WGSL padding code to append to the GPU struct to align it.
235
    ///
236
    /// This is useful if the struct needs to be bound directly with a dynamic
237
    /// bind group offset, which requires the offset to be a multiple of a GPU
238
    /// device specific alignment value.
239
    fn padding_code(alignment: u32) -> String;
240
}
241

242
impl<T: ShaderType> StorageType for T {
243
    fn aligned_size(alignment: u32) -> NonZeroU64 {
43✔
244
        NonZeroU64::new(next_multiple_of(T::min_size().get() as usize, alignment as usize) as u64)
43✔
245
            .unwrap()
246
    }
247

248
    fn padding_code(alignment: u32) -> String {
25✔
249
        let aligned_size = T::aligned_size(alignment);
25✔
250
        trace!(
25✔
NEW
251
            "Aligning {} to {} bytes as device limits requires. Orignal size: {} bytes. Aligned size: {} bytes.",
×
NEW
252
            std::any::type_name::<T>(),
×
253
            alignment,
×
NEW
254
            T::min_size().get(),
×
UNCOV
255
            aligned_size
×
256
        );
257

258
        // We need to pad the Spawner WGSL struct based on the device padding so that we
259
        // can use it as an array element but also has a direct struct binding.
260
        if T::min_size() != aligned_size {
25✔
261
            let padding_size = aligned_size.get() - T::min_size().get();
25✔
262
            assert!(padding_size % 4 == 0);
25✔
263
            format!("padding: array<u32, {}>", padding_size / 4)
25✔
264
        } else {
265
            "".to_string()
×
266
        }
267
    }
268
}
269

270
/// GPU representation of spawner parameters.
271
#[repr(C)]
272
#[derive(Debug, Default, Clone, Copy, Pod, Zeroable, ShaderType)]
273
pub(crate) struct GpuSpawnerParams {
274
    /// Transform of the effect (origin of the emitter). This is either added to
275
    /// emitted particles at spawn time, if the effect simulated in world
276
    /// space, or to all simulated particles if the effect is simulated in
277
    /// local space.
278
    transform: GpuCompressedTransform,
279
    /// Inverse of [`transform`], stored with the same convention.
280
    ///
281
    /// [`transform`]: crate::render::GpuSpawnerParams::transform
282
    inverse_transform: GpuCompressedTransform,
283
    /// Number of particles to spawn this frame.
284
    spawn: i32,
285
    /// Spawn seed, for randomized modifiers.
286
    seed: u32,
287
    /// Current number of used particles.
288
    count: i32,
289
    /// Index of the effect in the indirect dispatch and render buffers.
290
    effect_index: u32,
291
    /// The time in seconds that the cloned particles live, if this is a cloner.
292
    ///
293
    /// If this is a spawner, this value is zero.
294
    lifetime: f32,
295
    /// Padding.
296
    pad: [u32; 3],
297
}
298

299
#[repr(C)]
300
#[derive(Debug, Clone, Copy, Pod, Zeroable, ShaderType)]
301
pub struct GpuDispatchIndirect {
302
    pub x: u32,
303
    pub y: u32,
304
    pub z: u32,
305
    pub pong: u32,
306
}
307

308
impl Default for GpuDispatchIndirect {
309
    fn default() -> Self {
×
310
        Self {
311
            x: 0,
312
            y: 1,
313
            z: 1,
314
            pong: 0,
315
        }
316
    }
317
}
318

319
#[repr(C)]
320
#[derive(Debug, Default, Clone, Copy, Pod, Zeroable, ShaderType)]
321
pub struct GpuRenderEffectMetadata {
322
    pub ping: u32,
323
}
324

325
#[repr(C)]
326
#[derive(Debug, Default, Clone, Copy, Pod, Zeroable, ShaderType)]
327
pub struct GpuRenderGroupIndirect {
328
    pub vertex_count: u32,
329
    pub instance_count: u32,
330
    pub vertex_offset: i32,
331
    pub base_instance: u32,
332
    //
333
    pub alive_count: u32,
334
    pub max_update: u32,
335
    pub dead_count: u32,
336
    pub max_spawn: u32,
337
}
338

339
/// Stores metadata about each particle group.
340
///
341
/// This is written by the CPU and read by the GPU.
342
#[repr(C)]
343
#[derive(Debug, Default, Clone, Copy, Pod, Zeroable, ShaderType)]
344
pub struct GpuParticleGroup {
345
    /// The index of this particle group in the global particle group buffer.
346
    pub global_group_index: u32,
347
    /// The global index of the entire particle effect.
348
    pub effect_index: u32,
349
    /// The index of this effect in the group.
350
    ///
351
    /// For example, the first group in an effect has index 0, the second has
352
    /// index 1, etc.
353
    pub group_index_in_effect: u32,
354
    /// The index of the first particle in this group in the indirect index
355
    /// buffer.
356
    pub indirect_index: u32,
357
    /// The capacity of this group in number of particles.
358
    pub capacity: u32,
359
    /// The index of the first particle in this effect in the particle and
360
    /// indirect buffers.
361
    pub effect_particle_offset: u32,
362
}
363

364
/// Compute pipeline to run the `vfx_indirect` dispatch workgroup calculation
365
/// shader.
366
#[derive(Resource)]
367
pub(crate) struct DispatchIndirectPipeline {
368
    dispatch_indirect_layout: BindGroupLayout,
369
    pipeline: ComputePipeline,
370
}
371

372
impl FromWorld for DispatchIndirectPipeline {
373
    fn from_world(world: &mut World) -> Self {
1✔
374
        let render_device = world.get_resource::<RenderDevice>().unwrap();
1✔
375

376
        let storage_alignment = render_device.limits().min_storage_buffer_offset_alignment;
1✔
377
        let render_effect_indirect_size = GpuRenderEffectMetadata::aligned_size(storage_alignment);
1✔
378
        let render_group_indirect_size = GpuRenderGroupIndirect::aligned_size(storage_alignment);
1✔
379
        let dispatch_indirect_size = GpuDispatchIndirect::aligned_size(storage_alignment);
1✔
380
        let particle_group_size = GpuParticleGroup::aligned_size(storage_alignment);
1✔
381

382
        trace!(
1✔
383
            "GpuRenderEffectMetadata: min_size={} padded_size={} | GpuRenderGroupIndirect: min_size={} padded_size={} | \
×
384
            GpuDispatchIndirect: min_size={} padded_size={} | GpuParticleGroup: min_size={} padded_size={}",
×
385
            GpuRenderEffectMetadata::min_size(),
×
386
            render_effect_indirect_size,
×
387
            GpuRenderGroupIndirect::min_size(),
×
388
            render_group_indirect_size,
×
389
            GpuDispatchIndirect::min_size(),
×
390
            dispatch_indirect_size,
×
391
            GpuParticleGroup::min_size(),
×
392
            particle_group_size
393
        );
394
        let dispatch_indirect_layout = render_device.create_bind_group_layout(
1✔
395
            "hanabi:bind_group_layout:dispatch_indirect_dispatch_indirect",
396
            &[
1✔
397
                BindGroupLayoutEntry {
1✔
398
                    binding: 0,
1✔
399
                    visibility: ShaderStages::COMPUTE,
1✔
400
                    ty: BindingType::Buffer {
1✔
401
                        ty: BufferBindingType::Storage { read_only: false },
1✔
402
                        has_dynamic_offset: false,
1✔
403
                        min_binding_size: Some(render_effect_indirect_size),
1✔
404
                    },
405
                    count: None,
1✔
406
                },
407
                BindGroupLayoutEntry {
1✔
408
                    binding: 1,
1✔
409
                    visibility: ShaderStages::COMPUTE,
1✔
410
                    ty: BindingType::Buffer {
1✔
411
                        ty: BufferBindingType::Storage { read_only: false },
1✔
412
                        has_dynamic_offset: false,
1✔
413
                        min_binding_size: Some(render_group_indirect_size),
1✔
414
                    },
415
                    count: None,
1✔
416
                },
417
                BindGroupLayoutEntry {
1✔
418
                    binding: 2,
1✔
419
                    visibility: ShaderStages::COMPUTE,
1✔
420
                    ty: BindingType::Buffer {
1✔
421
                        ty: BufferBindingType::Storage { read_only: false },
1✔
422
                        has_dynamic_offset: false,
1✔
423
                        min_binding_size: Some(dispatch_indirect_size),
1✔
424
                    },
425
                    count: None,
1✔
426
                },
427
                BindGroupLayoutEntry {
1✔
428
                    binding: 3,
1✔
429
                    visibility: ShaderStages::COMPUTE,
1✔
430
                    ty: BindingType::Buffer {
1✔
431
                        ty: BufferBindingType::Storage { read_only: true },
1✔
432
                        has_dynamic_offset: false,
1✔
433
                        min_binding_size: Some(particle_group_size),
1✔
434
                    },
435
                    count: None,
1✔
436
                },
437
                BindGroupLayoutEntry {
1✔
438
                    binding: 4,
1✔
439
                    visibility: ShaderStages::COMPUTE,
1✔
440
                    ty: BindingType::Buffer {
1✔
441
                        ty: BufferBindingType::Storage { read_only: true },
1✔
442
                        has_dynamic_offset: false,
1✔
443
                        min_binding_size: Some(GpuSpawnerParams::min_size()),
1✔
444
                    },
445
                    count: None,
1✔
446
                },
447
            ],
448
        );
449

450
        trace!("GpuSimParams: min_size={}", GpuSimParams::min_size());
1✔
451
        let sim_params_layout = render_device.create_bind_group_layout(
1✔
452
            "hanabi:bind_group_layout:dispatch_indirect_sim_params",
453
            &[BindGroupLayoutEntry {
1✔
454
                binding: 0,
1✔
455
                visibility: ShaderStages::COMPUTE,
1✔
456
                ty: BindingType::Buffer {
1✔
457
                    ty: BufferBindingType::Uniform,
1✔
458
                    has_dynamic_offset: false,
1✔
459
                    min_binding_size: Some(GpuSimParams::min_size()),
1✔
460
                },
461
                count: None,
1✔
462
            }],
463
        );
464

465
        let pipeline_layout = render_device.create_pipeline_layout(&PipelineLayoutDescriptor {
1✔
466
            label: Some("hanabi:pipeline_layout:dispatch_indirect"),
1✔
467
            bind_group_layouts: &[&dispatch_indirect_layout, &sim_params_layout],
1✔
468
            push_constant_ranges: &[],
1✔
469
        });
470

471
        let render_effect_indirect_stride_code =
1✔
472
            (render_effect_indirect_size.get() as u32).to_wgsl_string();
1✔
473
        let render_group_indirect_stride_code =
1✔
474
            (render_group_indirect_size.get() as u32).to_wgsl_string();
1✔
475
        let dispatch_indirect_stride_code = (dispatch_indirect_size.get() as u32).to_wgsl_string();
1✔
476
        let indirect_code = include_str!("vfx_indirect.wgsl")
1✔
477
            .replace(
478
                "{{RENDER_EFFECT_INDIRECT_STRIDE}}",
479
                &render_effect_indirect_stride_code,
1✔
480
            )
481
            .replace(
482
                "{{RENDER_GROUP_INDIRECT_STRIDE}}",
483
                &render_group_indirect_stride_code,
1✔
484
            )
485
            .replace(
486
                "{{DISPATCH_INDIRECT_STRIDE}}",
487
                &dispatch_indirect_stride_code,
1✔
488
            );
489

490
        // Resolve imports. Because we don't insert this shader into Bevy' pipeline
491
        // cache, we don't get that part "for free", so we have to do it manually here.
492
        let indirect_naga_module = {
1✔
493
            let mut composer = Composer::default();
494

495
            // Import bevy_hanabi::vfx_common
496
            {
497
                let common_shader = HanabiPlugin::make_common_shader(
498
                    render_device.limits().min_storage_buffer_offset_alignment,
499
                );
500
                let mut desc: naga_oil::compose::ComposableModuleDescriptor<'_> =
501
                    (&common_shader).into();
502
                desc.shader_defs.insert(
503
                    "SPAWNER_PADDING".to_string(),
504
                    naga_oil::compose::ShaderDefValue::Bool(true),
505
                );
506
                let res = composer.add_composable_module(desc);
507
                assert!(res.is_ok());
508
            }
509

510
            let shader_defs = default();
1✔
511

512
            match composer.make_naga_module(NagaModuleDescriptor {
1✔
513
                source: &indirect_code,
1✔
514
                file_path: "vfx_indirect.wgsl",
1✔
515
                shader_defs,
1✔
516
                ..Default::default()
1✔
517
            }) {
518
                Ok(naga_module) => ShaderSource::Naga(Cow::Owned(naga_module)),
519
                Err(compose_error) => panic!(
×
520
                    "Failed to compose vfx_indirect.wgsl, naga_oil returned: {}",
521
                    compose_error.emit_to_string(&composer)
×
522
                ),
523
            }
524
        };
525

526
        debug!("Create indirect dispatch shader:\n{}", indirect_code);
1✔
527

528
        let shader_module = render_device.create_shader_module(ShaderModuleDescriptor {
1✔
529
            label: Some("hanabi:vfx_indirect_shader"),
1✔
530
            source: indirect_naga_module,
1✔
531
        });
532

533
        let pipeline = render_device.create_compute_pipeline(&RawComputePipelineDescriptor {
1✔
534
            label: Some("hanabi:compute_pipeline:dispatch_indirect"),
1✔
535
            layout: Some(&pipeline_layout),
1✔
536
            module: &shader_module,
1✔
537
            entry_point: "main",
1✔
538
            compilation_options: default(),
1✔
539
        });
540

541
        Self {
542
            dispatch_indirect_layout,
543
            pipeline,
544
        }
545
    }
546
}
547

548
#[derive(Resource)]
549
pub(crate) struct ParticlesInitPipeline {
550
    render_device: RenderDevice,
551
    sim_params_layout: BindGroupLayout,
552
    spawner_buffer_layout: BindGroupLayout,
553
    render_indirect_layout: BindGroupLayout,
554
}
555

556
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
557
pub(crate) struct ParticleInitPipelineKey {
558
    shader: Handle<Shader>,
559
    particle_layout_min_binding_size: NonZero<u64>,
560
    property_layout_min_binding_size: Option<NonZero<u64>>,
561
    flags: ParticleInitPipelineKeyFlags,
562
}
563

564
bitflags! {
565
    #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
566
    pub struct ParticleInitPipelineKeyFlags: u8 {
567
        const CLONE = 0x1;
568
        const ATTRIBUTE_PREV = 0x2;
569
        const ATTRIBUTE_NEXT = 0x4;
570
    }
571
}
572

573
impl FromWorld for ParticlesInitPipeline {
574
    fn from_world(world: &mut World) -> Self {
1✔
575
        let render_device = world.get_resource::<RenderDevice>().unwrap();
1✔
576

577
        let sim_params_layout = render_device.create_bind_group_layout(
1✔
578
            "hanabi:bind_group_layout:update_sim_params",
579
            &[BindGroupLayoutEntry {
1✔
580
                binding: 0,
1✔
581
                visibility: ShaderStages::COMPUTE,
1✔
582
                ty: BindingType::Buffer {
1✔
583
                    ty: BufferBindingType::Uniform,
1✔
584
                    has_dynamic_offset: false,
1✔
585
                    min_binding_size: Some(GpuSimParams::min_size()),
1✔
586
                },
587
                count: None,
1✔
588
            }],
589
        );
590

591
        let spawner_buffer_layout = render_device.create_bind_group_layout(
1✔
592
            "hanabi:buffer_layout:init_spawner",
593
            &[BindGroupLayoutEntry {
1✔
594
                binding: 0,
1✔
595
                visibility: ShaderStages::COMPUTE,
1✔
596
                ty: BindingType::Buffer {
1✔
597
                    ty: BufferBindingType::Storage { read_only: false },
1✔
598
                    has_dynamic_offset: true,
1✔
599
                    min_binding_size: Some(GpuSpawnerParams::min_size()),
1✔
600
                },
601
                count: None,
1✔
602
            }],
603
        );
604

605
        let render_indirect_layout = create_init_render_indirect_bind_group_layout(
606
            render_device,
1✔
607
            "hanabi:bind_group_layout:init_render_indirect",
608
            true,
609
        );
610

611
        Self {
612
            render_device: render_device.clone(),
1✔
613
            sim_params_layout,
614
            spawner_buffer_layout,
615
            render_indirect_layout,
616
        }
617
    }
618
}
619

620
impl SpecializedComputePipeline for ParticlesInitPipeline {
621
    type Key = ParticleInitPipelineKey;
622

623
    fn specialize(&self, key: Self::Key) -> ComputePipelineDescriptor {
×
624
        let particles_buffer_layout = create_init_particles_bind_group_layout(
625
            &self.render_device,
×
626
            "hanabi:init_particles_buffer_layout",
627
            key.particle_layout_min_binding_size,
×
628
            key.property_layout_min_binding_size,
×
629
        );
630

631
        let mut shader_defs = vec![];
×
632
        if key.flags.contains(ParticleInitPipelineKeyFlags::CLONE) {
×
633
            shader_defs.push(ShaderDefVal::Bool("CLONE".to_string(), true));
×
634
        }
635
        if key
×
636
            .flags
×
637
            .contains(ParticleInitPipelineKeyFlags::ATTRIBUTE_PREV)
×
638
        {
639
            shader_defs.push(ShaderDefVal::Bool("ATTRIBUTE_PREV".to_string(), true));
×
640
        }
641
        if key
×
642
            .flags
×
643
            .contains(ParticleInitPipelineKeyFlags::ATTRIBUTE_NEXT)
×
644
        {
645
            shader_defs.push(ShaderDefVal::Bool("ATTRIBUTE_NEXT".to_string(), true));
×
646
        }
647

648
        ComputePipelineDescriptor {
649
            label: Some("hanabi:pipeline_init_compute".into()),
×
650
            layout: vec![
×
651
                self.sim_params_layout.clone(),
652
                particles_buffer_layout,
653
                self.spawner_buffer_layout.clone(),
654
                self.render_indirect_layout.clone(),
655
            ],
656
            shader: key.shader,
×
657
            shader_defs,
658
            entry_point: "main".into(),
×
659
            push_constant_ranges: vec![],
×
660
        }
661
    }
662
}
663

664
#[derive(Resource)]
665
pub(crate) struct ParticlesUpdatePipeline {
666
    render_device: RenderDevice,
667
    sim_params_layout: BindGroupLayout,
668
    spawner_buffer_layout: BindGroupLayout,
669
    render_indirect_layout: BindGroupLayout,
670
}
671

672
impl FromWorld for ParticlesUpdatePipeline {
673
    fn from_world(world: &mut World) -> Self {
1✔
674
        let render_device = world.get_resource::<RenderDevice>().unwrap();
1✔
675

676
        let limits = render_device.limits();
1✔
677
        bevy::log::info!(
1✔
678
            "GPU limits:\n- max_compute_invocations_per_workgroup={}\n- max_compute_workgroup_size_x={}\n- max_compute_workgroup_size_y={}\n- max_compute_workgroup_size_z={}\n- max_compute_workgroups_per_dimension={}\n- min_storage_buffer_offset_alignment={}",
1✔
679
            limits.max_compute_invocations_per_workgroup, limits.max_compute_workgroup_size_x, limits.max_compute_workgroup_size_y, limits.max_compute_workgroup_size_z,
680
            limits.max_compute_workgroups_per_dimension, limits.min_storage_buffer_offset_alignment
681
        );
682

683
        trace!("GpuSimParams: min_size={}", GpuSimParams::min_size());
1✔
684
        let sim_params_layout = render_device.create_bind_group_layout(
1✔
685
            "hanabi:update_sim_params_layout",
686
            &[BindGroupLayoutEntry {
1✔
687
                binding: 0,
1✔
688
                visibility: ShaderStages::COMPUTE,
1✔
689
                ty: BindingType::Buffer {
1✔
690
                    ty: BufferBindingType::Uniform,
1✔
691
                    has_dynamic_offset: false,
1✔
692
                    min_binding_size: Some(GpuSimParams::min_size()),
1✔
693
                },
694
                count: None,
1✔
695
            }],
696
        );
697

698
        trace!(
1✔
699
            "GpuSpawnerParams: min_size={}",
×
700
            GpuSpawnerParams::min_size()
×
701
        );
702
        let spawner_buffer_layout = render_device.create_bind_group_layout(
1✔
703
            "hanabi:update_spawner_buffer_layout",
704
            &[BindGroupLayoutEntry {
1✔
705
                binding: 0,
1✔
706
                visibility: ShaderStages::COMPUTE,
1✔
707
                ty: BindingType::Buffer {
1✔
708
                    ty: BufferBindingType::Storage { read_only: false },
1✔
709
                    has_dynamic_offset: true,
1✔
710
                    min_binding_size: Some(GpuSpawnerParams::min_size()),
1✔
711
                },
712
                count: None,
1✔
713
            }],
714
        );
715

716
        let storage_alignment = render_device.limits().min_storage_buffer_offset_alignment;
1✔
717
        let render_effect_indirect_size = GpuRenderEffectMetadata::aligned_size(storage_alignment);
1✔
718
        let render_group_indirect_size = GpuRenderGroupIndirect::aligned_size(storage_alignment);
1✔
719
        trace!("GpuRenderEffectMetadata: min_size={} padded_size={} | GpuRenderGroupIndirect: min_size={} padded_size={}",
1✔
720
            GpuRenderEffectMetadata::min_size(),
×
721
            render_effect_indirect_size.get(),
×
722
            GpuRenderGroupIndirect::min_size(),
×
723
            render_group_indirect_size.get());
×
724
        let render_indirect_layout = render_device.create_bind_group_layout(
1✔
725
            "hanabi:update_render_indirect_layout",
726
            &[
1✔
727
                BindGroupLayoutEntry {
1✔
728
                    binding: 0,
1✔
729
                    visibility: ShaderStages::COMPUTE,
1✔
730
                    ty: BindingType::Buffer {
1✔
731
                        ty: BufferBindingType::Storage { read_only: false },
1✔
732
                        has_dynamic_offset: false,
1✔
733
                        min_binding_size: Some(render_effect_indirect_size),
1✔
734
                    },
735
                    count: None,
1✔
736
                },
737
                BindGroupLayoutEntry {
1✔
738
                    binding: 1,
1✔
739
                    visibility: ShaderStages::COMPUTE,
1✔
740
                    ty: BindingType::Buffer {
1✔
741
                        ty: BufferBindingType::Storage { read_only: false },
1✔
742
                        has_dynamic_offset: false,
1✔
743
                        // Array; needs padded size
744
                        min_binding_size: Some(render_group_indirect_size),
1✔
745
                    },
746
                    count: None,
1✔
747
                },
748
            ],
749
        );
750

751
        Self {
752
            render_device: render_device.clone(),
1✔
753
            sim_params_layout,
754
            spawner_buffer_layout,
755
            render_indirect_layout,
756
        }
757
    }
758
}
759

760
#[derive(Debug, Default, Clone, Hash, PartialEq, Eq)]
761
pub(crate) struct ParticleUpdatePipelineKey {
762
    /// Compute shader, with snippets applied, but not preprocessed yet.
763
    shader: Handle<Shader>,
764
    /// Particle layout.
765
    particle_layout: ParticleLayout,
766
    /// Property layout.
767
    property_layout: PropertyLayout,
768
    is_trail: bool,
769
}
770

771
impl SpecializedComputePipeline for ParticlesUpdatePipeline {
772
    type Key = ParticleUpdatePipelineKey;
773

774
    fn specialize(&self, key: Self::Key) -> ComputePipelineDescriptor {
×
775
        trace!(
×
776
            "GpuParticle: attributes.min_binding_size={} properties.min_binding_size={}",
×
777
            key.particle_layout.min_binding_size().get(),
×
778
            if key.property_layout.is_empty() {
×
779
                0
×
780
            } else {
781
                key.property_layout.min_binding_size().get()
×
782
            },
783
        );
784

785
        let update_particles_buffer_layout = create_update_bind_group_layout(
786
            &self.render_device,
787
            "hanabi:update_particles_buffer_layout",
788
            key.particle_layout.min_binding_size(),
789
            if key.property_layout.is_empty() {
790
                None
×
791
            } else {
792
                Some(key.property_layout.min_binding_size())
×
793
            },
794
        );
795

796
        let mut shader_defs = vec!["REM_MAX_SPAWN_ATOMIC".into()];
797
        if key.particle_layout.contains(Attribute::PREV) {
×
798
            shader_defs.push("ATTRIBUTE_PREV".into());
×
799
        }
800
        if key.particle_layout.contains(Attribute::NEXT) {
×
801
            shader_defs.push("ATTRIBUTE_NEXT".into());
×
802
        }
803
        if key.is_trail {
×
804
            shader_defs.push("TRAIL".into());
×
805
        }
806

807
        ComputePipelineDescriptor {
808
            label: Some("hanabi:pipeline_update_compute".into()),
809
            layout: vec![
810
                self.sim_params_layout.clone(),
811
                update_particles_buffer_layout,
812
                self.spawner_buffer_layout.clone(),
813
                self.render_indirect_layout.clone(),
814
            ],
815
            shader: key.shader,
816
            shader_defs,
817
            entry_point: "main".into(),
818
            push_constant_ranges: Vec::new(),
819
        }
820
    }
821
}
822

823
#[derive(Resource)]
824
pub(crate) struct ParticlesRenderPipeline {
825
    render_device: RenderDevice,
826
    view_layout: BindGroupLayout,
827
    material_layouts: HashMap<TextureLayout, BindGroupLayout>,
828
}
829

830
impl ParticlesRenderPipeline {
831
    /// Cache a material, creating its bind group layout based on the texture
832
    /// layout.
833
    pub fn cache_material(&mut self, layout: &TextureLayout) {
×
834
        if layout.layout.is_empty() {
×
835
            return;
×
836
        }
837

838
        // FIXME - no current stable API to insert an entry into a HashMap only if it
839
        // doesn't exist, and without having to build a key (as opposed to a reference).
840
        // So do 2 lookups instead, to avoid having to clone the layout if it's already
841
        // cached (which should be the common case).
842
        if self.material_layouts.contains_key(layout) {
×
843
            return;
×
844
        }
845

846
        let mut entries = Vec::with_capacity(layout.layout.len() * 2);
×
847
        let mut index = 0;
×
848
        for _slot in &layout.layout {
×
849
            entries.push(BindGroupLayoutEntry {
×
850
                binding: index,
×
851
                visibility: ShaderStages::FRAGMENT,
×
852
                ty: BindingType::Texture {
×
853
                    multisampled: false,
×
854
                    sample_type: TextureSampleType::Float { filterable: true },
×
855
                    view_dimension: TextureViewDimension::D2,
×
856
                },
857
                count: None,
×
858
            });
859
            entries.push(BindGroupLayoutEntry {
×
860
                binding: index + 1,
×
861
                visibility: ShaderStages::FRAGMENT,
×
862
                ty: BindingType::Sampler(SamplerBindingType::Filtering),
×
863
                count: None,
×
864
            });
865
            index += 2;
×
866
        }
867
        let material_bind_group_layout = self
×
868
            .render_device
×
869
            .create_bind_group_layout("hanabi:material_layout_render", &entries[..]);
×
870

871
        self.material_layouts
×
872
            .insert(layout.clone(), material_bind_group_layout);
×
873
    }
874

875
    /// Retrieve a bind group layout for a cached material.
876
    pub fn get_material(&self, layout: &TextureLayout) -> Option<&BindGroupLayout> {
×
877
        // Prevent a hash and lookup for the trivial case of an empty layout
878
        if layout.layout.is_empty() {
×
879
            return None;
×
880
        }
881

882
        self.material_layouts.get(layout)
×
883
    }
884
}
885

886
impl FromWorld for ParticlesRenderPipeline {
887
    fn from_world(world: &mut World) -> Self {
1✔
888
        let render_device = world.get_resource::<RenderDevice>().unwrap();
1✔
889

890
        let view_layout = render_device.create_bind_group_layout(
1✔
891
            "hanabi:view_layout_render",
892
            &[
1✔
893
                BindGroupLayoutEntry {
1✔
894
                    binding: 0,
1✔
895
                    visibility: ShaderStages::VERTEX_FRAGMENT,
1✔
896
                    ty: BindingType::Buffer {
1✔
897
                        ty: BufferBindingType::Uniform,
1✔
898
                        has_dynamic_offset: true,
1✔
899
                        min_binding_size: Some(ViewUniform::min_size()),
1✔
900
                    },
901
                    count: None,
1✔
902
                },
903
                BindGroupLayoutEntry {
1✔
904
                    binding: 1,
1✔
905
                    visibility: ShaderStages::VERTEX_FRAGMENT,
1✔
906
                    ty: BindingType::Buffer {
1✔
907
                        ty: BufferBindingType::Uniform,
1✔
908
                        has_dynamic_offset: false,
1✔
909
                        min_binding_size: Some(GpuSimParams::min_size()),
1✔
910
                    },
911
                    count: None,
1✔
912
                },
913
            ],
914
        );
915

916
        Self {
917
            render_device: render_device.clone(),
1✔
918
            view_layout,
919
            material_layouts: default(),
1✔
920
        }
921
    }
922
}
923

924
#[cfg(all(feature = "2d", feature = "3d"))]
925
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
926
enum PipelineMode {
927
    Camera2d,
928
    Camera3d,
929
}
930

931
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
932
pub(crate) struct ParticleRenderPipelineKey {
933
    /// Render shader, with snippets applied, but not preprocessed yet.
934
    shader: Handle<Shader>,
935
    /// Particle layout.
936
    particle_layout: ParticleLayout,
937
    /// Texture layout.
938
    texture_layout: TextureLayout,
939
    /// Key: LOCAL_SPACE_SIMULATION
940
    /// The effect is simulated in local space, and during rendering all
941
    /// particles are transformed by the effect's [`GlobalTransform`].
942
    local_space_simulation: bool,
943
    /// Key: USE_ALPHA_MASK
944
    /// The effect is rendered with alpha masking.
945
    use_alpha_mask: bool,
946
    /// The effect needs Alpha blend.
947
    alpha_mode: AlphaMode,
948
    /// Key: FLIPBOOK
949
    /// The effect is rendered with flipbook texture animation based on the
950
    /// sprite index of each particle.
951
    flipbook: bool,
952
    /// Key: NEEDS_UV
953
    /// The effect needs UVs.
954
    needs_uv: bool,
955
    /// Key: RIBBONS
956
    /// The effect has ribbons.
957
    ribbons: bool,
958
    /// For dual-mode configurations only, the actual mode of the current render
959
    /// pipeline. Otherwise the mode is implicitly determined by the active
960
    /// feature.
961
    #[cfg(all(feature = "2d", feature = "3d"))]
962
    pipeline_mode: PipelineMode,
963
    /// MSAA sample count.
964
    msaa_samples: u32,
965
    /// Is the camera using an HDR render target?
966
    hdr: bool,
967
}
968

969
impl Default for ParticleRenderPipelineKey {
970
    fn default() -> Self {
×
971
        Self {
972
            shader: Handle::default(),
×
973
            particle_layout: ParticleLayout::empty(),
×
974
            texture_layout: default(),
×
975
            local_space_simulation: false,
976
            use_alpha_mask: false,
977
            alpha_mode: AlphaMode::Blend,
978
            flipbook: false,
979
            needs_uv: false,
980
            ribbons: false,
981
            #[cfg(all(feature = "2d", feature = "3d"))]
982
            pipeline_mode: PipelineMode::Camera3d,
983
            msaa_samples: Msaa::default().samples(),
×
984
            hdr: false,
985
        }
986
    }
987
}
988

989
impl SpecializedRenderPipeline for ParticlesRenderPipeline {
990
    type Key = ParticleRenderPipelineKey;
991

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

995
        // Base mandatory part of vertex buffer layout
996
        let vertex_buffer_layout = VertexBufferLayout {
997
            array_stride: 20,
998
            step_mode: VertexStepMode::Vertex,
999
            attributes: vec![
×
1000
                //  @location(0) vertex_position: vec3<f32>
1001
                VertexAttribute {
1002
                    format: VertexFormat::Float32x3,
1003
                    offset: 0,
1004
                    shader_location: 0,
1005
                },
1006
                //  @location(1) vertex_uv: vec2<f32>
1007
                VertexAttribute {
1008
                    format: VertexFormat::Float32x2,
1009
                    offset: 12,
1010
                    shader_location: 1,
1011
                },
1012
                //  @location(1) vertex_color: u32
1013
                // VertexAttribute {
1014
                //     format: VertexFormat::Uint32,
1015
                //     offset: 12,
1016
                //     shader_location: 1,
1017
                // },
1018
                //  @location(2) vertex_velocity: vec3<f32>
1019
                // VertexAttribute {
1020
                //     format: VertexFormat::Float32x3,
1021
                //     offset: 12,
1022
                //     shader_location: 1,
1023
                // },
1024
                //  @location(3) vertex_uv: vec2<f32>
1025
                // VertexAttribute {
1026
                //     format: VertexFormat::Float32x2,
1027
                //     offset: 28,
1028
                //     shader_location: 3,
1029
                // },
1030
            ],
1031
        };
1032

1033
        let dispatch_indirect_size = GpuDispatchIndirect::aligned_size(
1034
            self.render_device
×
1035
                .limits()
×
1036
                .min_storage_buffer_offset_alignment,
×
1037
        );
1038
        let mut entries = vec![
×
1039
            BindGroupLayoutEntry {
×
1040
                binding: 0,
×
1041
                visibility: ShaderStages::VERTEX,
×
1042
                ty: BindingType::Buffer {
×
1043
                    ty: BufferBindingType::Storage { read_only: true },
×
1044
                    has_dynamic_offset: false,
×
1045
                    min_binding_size: Some(key.particle_layout.min_binding_size()),
×
1046
                },
1047
                count: None,
×
1048
            },
1049
            BindGroupLayoutEntry {
×
1050
                binding: 1,
×
1051
                visibility: ShaderStages::VERTEX,
×
1052
                ty: BindingType::Buffer {
×
1053
                    ty: BufferBindingType::Storage { read_only: true },
×
1054
                    has_dynamic_offset: false,
×
1055
                    min_binding_size: BufferSize::new(4u64),
×
1056
                },
1057
                count: None,
×
1058
            },
1059
            BindGroupLayoutEntry {
×
1060
                binding: 2,
×
1061
                visibility: ShaderStages::VERTEX,
×
1062
                ty: BindingType::Buffer {
×
1063
                    ty: BufferBindingType::Storage { read_only: true },
×
1064
                    has_dynamic_offset: true,
×
1065
                    min_binding_size: Some(dispatch_indirect_size),
×
1066
                },
1067
                count: None,
×
1068
            },
1069
        ];
1070
        if key.local_space_simulation {
×
1071
            entries.push(BindGroupLayoutEntry {
×
1072
                binding: 3,
×
1073
                visibility: ShaderStages::VERTEX,
×
1074
                ty: BindingType::Buffer {
×
1075
                    ty: BufferBindingType::Storage { read_only: true },
×
1076
                    has_dynamic_offset: true,
×
1077
                    min_binding_size: Some(GpuSpawnerParams::min_size()),
×
1078
                },
1079
                count: None,
×
1080
            });
1081
        }
1082

1083
        trace!(
1084
            "GpuParticle: layout.min_binding_size={}",
×
1085
            key.particle_layout.min_binding_size()
×
1086
        );
1087
        trace!(
×
1088
            "Creating render bind group layout with {} entries",
×
1089
            entries.len()
×
1090
        );
1091
        let particles_buffer_layout = self
×
1092
            .render_device
×
1093
            .create_bind_group_layout("hanabi:buffer_layout_render", &entries);
×
1094

1095
        let mut layout = vec![self.view_layout.clone(), particles_buffer_layout];
×
1096
        let mut shader_defs = vec!["SPAWNER_READONLY".into()];
×
1097

1098
        if let Some(material_bind_group_layout) = self.get_material(&key.texture_layout) {
×
1099
            layout.push(material_bind_group_layout.clone());
1100
            // //  @location(1) vertex_uv: vec2<f32>
1101
            // vertex_buffer_layout.attributes.push(VertexAttribute {
1102
            //     format: VertexFormat::Float32x2,
1103
            //     offset: 12,
1104
            //     shader_location: 1,
1105
            // });
1106
            // vertex_buffer_layout.array_stride += 8;
1107
        }
1108

1109
        // Key: LOCAL_SPACE_SIMULATION
1110
        if key.local_space_simulation {
×
1111
            shader_defs.push("LOCAL_SPACE_SIMULATION".into());
×
1112
            shader_defs.push("RENDER_NEEDS_SPAWNER".into());
×
1113
        }
1114

1115
        // Key: USE_ALPHA_MASK
1116
        if key.use_alpha_mask {
×
1117
            shader_defs.push("USE_ALPHA_MASK".into());
×
1118
        }
1119

1120
        // Key: FLIPBOOK
1121
        if key.flipbook {
×
1122
            shader_defs.push("FLIPBOOK".into());
×
1123
        }
1124

1125
        // Key: NEEDS_UV
1126
        if key.needs_uv {
×
1127
            shader_defs.push("NEEDS_UV".into());
×
1128
        }
1129

1130
        // Key: RIBBONS
1131
        if key.ribbons {
×
1132
            shader_defs.push("RIBBONS".into());
×
1133
        }
1134

1135
        #[cfg(all(feature = "2d", feature = "3d"))]
1136
        let depth_stencil = match key.pipeline_mode {
1137
            // Bevy's Transparent2d render phase doesn't support a depth-stencil buffer.
1138
            PipelineMode::Camera2d => None,
×
1139
            PipelineMode::Camera3d => Some(DepthStencilState {
×
1140
                format: TextureFormat::Depth32Float,
×
1141
                // Use depth buffer with alpha-masked particles, not with transparent ones
1142
                depth_write_enabled: key.use_alpha_mask,
×
1143
                // Bevy uses reverse-Z, so Greater really means closer
1144
                depth_compare: CompareFunction::Greater,
×
1145
                stencil: StencilState::default(),
×
1146
                bias: DepthBiasState::default(),
×
1147
            }),
1148
        };
1149

1150
        #[cfg(all(feature = "2d", not(feature = "3d")))]
1151
        let depth_stencil: Option<DepthStencilState> = None;
1152

1153
        #[cfg(all(feature = "3d", not(feature = "2d")))]
1154
        let depth_stencil = Some(DepthStencilState {
1155
            format: TextureFormat::Depth32Float,
1156
            // Use depth buffer with alpha-masked particles, not with transparent ones
1157
            depth_write_enabled: key.use_alpha_mask,
1158
            // Bevy uses reverse-Z, so Greater really means closer
1159
            depth_compare: CompareFunction::Greater,
1160
            stencil: StencilState::default(),
1161
            bias: DepthBiasState::default(),
1162
        });
1163

1164
        let format = if key.hdr {
1165
            ViewTarget::TEXTURE_FORMAT_HDR
×
1166
        } else {
1167
            TextureFormat::bevy_default()
×
1168
        };
1169

1170
        RenderPipelineDescriptor {
1171
            vertex: VertexState {
1172
                shader: key.shader.clone(),
1173
                entry_point: "vertex".into(),
1174
                shader_defs: shader_defs.clone(),
1175
                buffers: vec![vertex_buffer_layout],
1176
            },
1177
            fragment: Some(FragmentState {
1178
                shader: key.shader,
1179
                shader_defs,
1180
                entry_point: "fragment".into(),
1181
                targets: vec![Some(ColorTargetState {
1182
                    format,
1183
                    blend: Some(key.alpha_mode.into()),
1184
                    write_mask: ColorWrites::ALL,
1185
                })],
1186
            }),
1187
            layout,
1188
            primitive: PrimitiveState {
1189
                front_face: FrontFace::Ccw,
1190
                cull_mode: None,
1191
                unclipped_depth: false,
1192
                polygon_mode: PolygonMode::Fill,
1193
                conservative: false,
1194
                topology: PrimitiveTopology::TriangleList,
1195
                strip_index_format: None,
1196
            },
1197
            depth_stencil,
1198
            multisample: MultisampleState {
1199
                count: key.msaa_samples,
1200
                mask: !0,
1201
                alpha_to_coverage_enabled: false,
1202
            },
1203
            label: Some("hanabi:pipeline_render".into()),
1204
            push_constant_ranges: Vec::new(),
1205
        }
1206
    }
1207
}
1208

1209
/// A single effect instance extracted from a [`ParticleEffect`] as a
1210
/// render world item.
1211
///
1212
/// [`ParticleEffect`]: crate::ParticleEffect
1213
#[derive(Debug, Component)]
1214
pub(crate) struct ExtractedEffect {
1215
    /// Handle to the effect asset this instance is based on.
1216
    /// The handle is weak to prevent refcount cycles and gracefully handle
1217
    /// assets unloaded or destroyed after a draw call has been submitted.
1218
    pub handle: Handle<EffectAsset>,
1219
    /// Particle layout for the effect.
1220
    #[allow(dead_code)]
1221
    pub particle_layout: ParticleLayout,
1222
    /// Property layout for the effect.
1223
    pub property_layout: PropertyLayout,
1224
    /// Values of properties written in a binary blob according to
1225
    /// [`property_layout`].
1226
    ///
1227
    /// This is `Some(blob)` if the data needs to be (re)uploaded to GPU, or
1228
    /// `None` if nothing needs to be done for this frame.
1229
    ///
1230
    /// [`property_layout`]: crate::render::ExtractedEffect::property_layout
1231
    pub property_data: Option<Vec<u8>>,
1232
    /// Maps a group number to the runtime initializer for that group.
1233
    ///
1234
    /// Obtained from calling [`EffectSpawner::tick()`] on the source effect
1235
    /// instance.
1236
    ///
1237
    /// [`EffectSpawner::tick()`]: crate::EffectSpawner::tick
1238
    pub initializers: Vec<EffectInitializer>,
1239
    /// Global transform of the effect origin, extracted from the
1240
    /// [`GlobalTransform`].
1241
    pub transform: Mat4,
1242
    /// Inverse global transform of the effect origin, extracted from the
1243
    /// [`GlobalTransform`].
1244
    pub inverse_transform: Mat4,
1245
    /// Layout flags.
1246
    pub layout_flags: LayoutFlags,
1247
    /// Texture layout.
1248
    pub texture_layout: TextureLayout,
1249
    /// Textures.
1250
    pub textures: Vec<Handle<Image>>,
1251
    /// Alpha mode.
1252
    pub alpha_mode: AlphaMode,
1253
    /// Effect shaders.
1254
    pub effect_shaders: Vec<EffectShader>,
1255
    /// For 2D rendering, the Z coordinate used as the sort key. Ignored for 3D
1256
    /// rendering.
1257
    #[cfg(feature = "2d")]
1258
    pub z_sort_key_2d: FloatOrd,
1259
}
1260

1261
/// Extracted data for newly-added [`ParticleEffect`] component requiring a new
1262
/// GPU allocation.
1263
///
1264
/// [`ParticleEffect`]: crate::ParticleEffect
1265
pub struct AddedEffect {
1266
    /// Entity with a newly-added [`ParticleEffect`] component.
1267
    ///
1268
    /// [`ParticleEffect`]: crate::ParticleEffect
1269
    pub entity: Entity,
1270
    pub groups: Vec<AddedEffectGroup>,
1271
    /// Layout of particle attributes.
1272
    pub particle_layout: ParticleLayout,
1273
    /// Layout of properties for the effect, if properties are used at all, or
1274
    /// an empty layout.
1275
    pub property_layout: PropertyLayout,
1276
    pub layout_flags: LayoutFlags,
1277
    /// Handle of the effect asset.
1278
    pub handle: Handle<EffectAsset>,
1279
    /// The order in which we evaluate groups.
1280
    pub group_order: Vec<u32>,
1281
}
1282

1283
pub struct AddedEffectGroup {
1284
    pub capacity: u32,
1285
    pub src_group_index_if_trail: Option<u32>,
1286
}
1287

1288
/// Collection of all extracted effects for this frame, inserted into the
1289
/// render world as a render resource.
1290
#[derive(Default, Resource)]
1291
pub(crate) struct ExtractedEffects {
1292
    /// Map of extracted effects from the entity the source [`ParticleEffect`]
1293
    /// is on.
1294
    ///
1295
    /// [`ParticleEffect`]: crate::ParticleEffect
1296
    pub effects: HashMap<Entity, ExtractedEffect>,
1297
    /// Entites which had their [`ParticleEffect`] component removed.
1298
    ///
1299
    /// [`ParticleEffect`]: crate::ParticleEffect
1300
    pub removed_effect_entities: Vec<Entity>,
1301
    /// Newly added effects without a GPU allocation yet.
1302
    pub added_effects: Vec<AddedEffect>,
1303
}
1304

1305
#[derive(Default, Resource)]
1306
pub(crate) struct EffectAssetEvents {
1307
    pub images: Vec<AssetEvent<Image>>,
1308
}
1309

1310
/// System extracting all the asset events for the [`Image`] assets to enable
1311
/// dynamic update of images bound to any effect.
1312
///
1313
/// This system runs in parallel of [`extract_effects`].
1314
pub(crate) fn extract_effect_events(
10✔
1315
    mut events: ResMut<EffectAssetEvents>,
1316
    mut image_events: Extract<EventReader<AssetEvent<Image>>>,
1317
) {
1318
    trace!("extract_effect_events");
10✔
1319

1320
    let EffectAssetEvents { ref mut images } = *events;
10✔
1321
    *images = image_events.read().copied().collect();
10✔
1322
}
1323

1324
/// System extracting data for rendering of all active [`ParticleEffect`]
1325
/// components.
1326
///
1327
/// Extract rendering data for all [`ParticleEffect`] components in the world
1328
/// which are visible ([`ComputedVisibility::is_visible`] is `true`), and wrap
1329
/// the data into a new [`ExtractedEffect`] instance added to the
1330
/// [`ExtractedEffects`] resource.
1331
///
1332
/// This system runs in parallel of [`extract_effect_events`].
1333
///
1334
/// [`ParticleEffect`]: crate::ParticleEffect
1335
pub(crate) fn extract_effects(
10✔
1336
    real_time: Extract<Res<Time<Real>>>,
1337
    virtual_time: Extract<Res<Time<Virtual>>>,
1338
    time: Extract<Res<Time<EffectSimulation>>>,
1339
    effects: Extract<Res<Assets<EffectAsset>>>,
1340
    _images: Extract<Res<Assets<Image>>>,
1341
    mut query: Extract<
1342
        ParamSet<(
1343
            // All existing ParticleEffect components
1344
            Query<(
1345
                Entity,
1346
                Option<&InheritedVisibility>,
1347
                Option<&ViewVisibility>,
1348
                &EffectInitializers,
1349
                &CompiledParticleEffect,
1350
                Option<Ref<EffectProperties>>,
1351
                &GlobalTransform,
1352
            )>,
1353
            // Newly added ParticleEffect components
1354
            Query<
1355
                (Entity, &CompiledParticleEffect),
1356
                (Added<CompiledParticleEffect>, With<GlobalTransform>),
1357
            >,
1358
        )>,
1359
    >,
1360
    mut removed_effects_event_reader: Extract<EventReader<RemovedEffectsEvent>>,
1361
    mut sim_params: ResMut<SimParams>,
1362
    mut extracted_effects: ResMut<ExtractedEffects>,
1363
) {
1364
    trace!("extract_effects");
10✔
1365

1366
    // Save simulation params into render world
1367
    sim_params.time = time.elapsed_seconds_f64();
10✔
1368
    sim_params.delta_time = time.delta_seconds();
10✔
1369
    sim_params.virtual_time = virtual_time.elapsed_seconds_f64();
10✔
1370
    sim_params.virtual_delta_time = virtual_time.delta_seconds();
10✔
1371
    sim_params.real_time = real_time.elapsed_seconds_f64();
10✔
1372
    sim_params.real_delta_time = real_time.delta_seconds();
10✔
1373

1374
    // Collect removed effects for later GPU data purge
1375
    extracted_effects.removed_effect_entities =
10✔
1376
        removed_effects_event_reader
10✔
1377
            .read()
10✔
1378
            .fold(vec![], |mut acc, ev| {
10✔
1379
                // FIXME - Need to clone because we can't consume the event, we only have
1380
                // read-only access to the main world
1381
                acc.append(&mut ev.entities.clone());
×
1382
                acc
×
1383
            });
1384
    trace!(
NEW
1385
        "Found {} removed effect(s).",
×
1386
        extracted_effects.removed_effect_entities.len()
×
1387
    );
1388

1389
    // Collect added effects for later GPU data allocation
1390
    extracted_effects.added_effects = query
10✔
1391
        .p1()
10✔
1392
        .iter()
10✔
1393
        .filter_map(|(entity, compiled_effect)| {
11✔
1394
            let handle = compiled_effect.asset.clone_weak();
1✔
1395
            let asset = effects.get(&compiled_effect.asset)?;
2✔
1396
            let particle_layout = asset.particle_layout();
1397
            assert!(
1398
                particle_layout.size() > 0,
1399
                "Invalid empty particle layout for effect '{}' on entity {:?}. Did you forget to add some modifier to the asset?",
×
1400
                asset.name,
1401
                entity
1402
            );
1403
            let property_layout = asset.property_layout();
×
1404
            let group_order = asset.calculate_group_order();
×
1405

1406
            trace!(
×
1407
                "Found new effect: entity {:?} | capacities {:?} | particle_layout {:?} | \
×
1408
                 property_layout {:?} | layout_flags {:?}",
×
1409
                 entity,
×
1410
                 asset.capacities(),
×
1411
                 particle_layout,
1412
                 property_layout,
1413
                 compiled_effect.layout_flags);
1414

1415
            Some(AddedEffect {
×
1416
                entity,
×
1417
                groups: asset.capacities().iter().zip(asset.init.iter()).map(|(&capacity, init)| {
×
1418
                    AddedEffectGroup {
×
1419
                        capacity,
×
1420
                        src_group_index_if_trail: match init {
×
1421
                            Initializer::Spawner(_) => None,
×
1422
                            Initializer::Cloner(cloner) => Some(cloner.src_group_index),
×
1423
                        }
1424
                    }
1425
                }).collect(),
×
1426
                particle_layout,
1427
                property_layout,
1428
                group_order,
1429
                layout_flags: compiled_effect.layout_flags,
1430
                handle,
1431
            })
1432
        })
1433
        .collect();
1434

1435
    // Loop over all existing effects to extract them
1436
    extracted_effects.effects.clear();
1437
    for (
1438
        entity,
×
1439
        maybe_inherited_visibility,
×
1440
        maybe_view_visibility,
×
1441
        initializers,
×
1442
        effect,
×
1443
        maybe_properties,
×
1444
        transform,
×
1445
    ) in query.p0().iter_mut()
1446
    {
1447
        // Check if shaders are configured
NEW
1448
        let effect_shaders = effect.get_configured_shaders();
×
1449
        if effect_shaders.is_empty() {
×
1450
            continue;
×
1451
        }
1452

1453
        // Check if hidden, unless always simulated
1454
        if effect.simulation_condition == SimulationCondition::WhenVisible
×
1455
            && !maybe_inherited_visibility
×
1456
                .map(|cv| cv.get())
×
1457
                .unwrap_or(true)
×
1458
            && !maybe_view_visibility.map(|cv| cv.get()).unwrap_or(true)
×
1459
        {
1460
            continue;
×
1461
        }
1462

1463
        // Check if asset is available, otherwise silently ignore
1464
        let Some(asset) = effects.get(&effect.asset) else {
×
1465
            trace!(
×
1466
                "EffectAsset not ready; skipping ParticleEffect instance on entity {:?}.",
×
1467
                entity
1468
            );
1469
            continue;
×
1470
        };
1471

1472
        #[cfg(feature = "2d")]
1473
        let z_sort_key_2d = effect.z_layer_2d;
1474

1475
        let property_layout = asset.property_layout();
1476
        let texture_layout = asset.module().texture_layout();
1477

1478
        let property_data = if let Some(properties) = maybe_properties {
×
1479
            // Note: must check that property layout is not empty, because the
1480
            // EffectProperties component is marked as changed when added but contains an
1481
            // empty Vec if there's no property, which would later raise an error if we
1482
            // don't return None here.
1483
            if properties.is_changed() && !property_layout.is_empty() {
×
1484
                trace!("Detected property change, re-serializing...");
×
1485
                Some(properties.serialize(&property_layout))
×
1486
            } else {
1487
                None
×
1488
            }
1489
        } else {
1490
            None
×
1491
        };
1492

1493
        let layout_flags = effect.layout_flags;
1494
        let alpha_mode = effect.alpha_mode;
1495

1496
        trace!(
1497
            "Extracted instance of effect '{}' on entity {:?}: texture_layout_count={} texture_count={} layout_flags={:?}",
×
1498
            asset.name,
×
1499
            entity,
×
1500
            texture_layout.layout.len(),
×
1501
            effect.textures.len(),
×
1502
            layout_flags,
1503
        );
1504

1505
        extracted_effects.effects.insert(
×
1506
            entity,
×
1507
            ExtractedEffect {
×
1508
                handle: effect.asset.clone_weak(),
×
1509
                particle_layout: asset.particle_layout().clone(),
×
1510
                property_layout,
×
1511
                property_data,
×
1512
                initializers: initializers.0.clone(),
×
1513
                transform: transform.compute_matrix(),
×
1514
                // TODO - more efficient/correct way than inverse()?
1515
                inverse_transform: transform.compute_matrix().inverse(),
×
1516
                layout_flags,
×
1517
                texture_layout,
×
1518
                textures: effect.textures.clone(),
×
1519
                alpha_mode,
×
NEW
1520
                effect_shaders: effect_shaders.to_vec(),
×
1521
                #[cfg(feature = "2d")]
×
1522
                z_sort_key_2d,
×
1523
            },
1524
        );
1525
    }
1526
}
1527

1528
/// GPU representation of a single vertex of a particle mesh stored in a GPU
1529
/// buffer.
1530
#[repr(C)]
1531
#[derive(Copy, Clone, Pod, Zeroable, ShaderType)]
1532
struct GpuParticleVertex {
1533
    /// Vertex position.
1534
    pub position: [f32; 3],
1535
    /// UV coordinates of vertex.
1536
    pub uv: [f32; 2],
1537
}
1538

1539
/// Various GPU limits and aligned sizes computed once and cached.
1540
struct GpuLimits {
1541
    /// Value of [`WgpuLimits::min_storage_buffer_offset_alignment`].
1542
    ///
1543
    /// [`WgpuLimits::min_storage_buffer_offset_alignment`]: bevy::render::settings::WgpuLimits::min_storage_buffer_offset_alignment
1544
    storage_buffer_align: NonZeroU32,
1545

1546
    /// Size of [`GpuDispatchIndirect`] aligned to the contraint of
1547
    /// [`WgpuLimits::min_storage_buffer_offset_alignment`].
1548
    ///
1549
    /// [`WgpuLimits::min_storage_buffer_offset_alignment`]: bevy::render::settings::WgpuLimits::min_storage_buffer_offset_alignment
1550
    dispatch_indirect_aligned_size: NonZeroU32,
1551

1552
    /// Size of [`GpuRenderEffectMetadata`] aligned to the contraint of
1553
    /// [`WgpuLimits::min_storage_buffer_offset_alignment`].
1554
    ///
1555
    /// [`WgpuLimits::min_storage_buffer_offset_alignment`]: bevy::render::settings::WgpuLimits::min_storage_buffer_offset_alignment
1556
    render_effect_indirect_aligned_size: NonZeroU32,
1557

1558
    /// Size of [`GpuRenderGroupIndirect`] aligned to the contraint of
1559
    /// [`WgpuLimits::min_storage_buffer_offset_alignment`].
1560
    ///
1561
    /// [`WgpuLimits::min_storage_buffer_offset_alignment`]: bevy::render::settings::WgpuLimits::min_storage_buffer_offset_alignment
1562
    render_group_indirect_aligned_size: NonZeroU32,
1563

1564
    /// Size of [`GpuParticleGroup`] aligned to the contraint of
1565
    /// [`WgpuLimits::min_storage_buffer_offset_alignment`].
1566
    ///
1567
    /// [`WgpuLimits::min_storage_buffer_offset_alignment`]: bevy::render::settings::WgpuLimits::min_storage_buffer_offset_alignment
1568
    particle_group_aligned_size: NonZeroU32,
1569
}
1570

1571
impl GpuLimits {
1572
    pub fn from_device(render_device: &RenderDevice) -> Self {
2✔
1573
        let storage_buffer_align = render_device.limits().min_storage_buffer_offset_alignment;
2✔
1574

1575
        let dispatch_indirect_aligned_size = NonZeroU32::new(next_multiple_of(
2✔
1576
            GpuDispatchIndirect::min_size().get() as usize,
2✔
1577
            storage_buffer_align as usize,
2✔
1578
        ) as u32)
2✔
1579
        .unwrap();
1580

1581
        let render_effect_indirect_aligned_size = NonZeroU32::new(next_multiple_of(
2✔
1582
            GpuRenderEffectMetadata::min_size().get() as usize,
2✔
1583
            storage_buffer_align as usize,
2✔
1584
        ) as u32)
2✔
1585
        .unwrap();
1586

1587
        let render_group_indirect_aligned_size = NonZeroU32::new(next_multiple_of(
2✔
1588
            GpuRenderGroupIndirect::min_size().get() as usize,
2✔
1589
            storage_buffer_align as usize,
2✔
1590
        ) as u32)
2✔
1591
        .unwrap();
1592

1593
        let particle_group_aligned_size = NonZeroU32::new(next_multiple_of(
2✔
1594
            GpuParticleGroup::min_size().get() as usize,
2✔
1595
            storage_buffer_align as usize,
2✔
1596
        ) as u32)
2✔
1597
        .unwrap();
1598

1599
        trace!(
2✔
1600
            "GpuLimits: storage_buffer_align={} gpu_dispatch_indirect_aligned_size={} \
×
1601
            gpu_render_effect_indirect_aligned_size={} gpu_render_group_indirect_aligned_size={}",
×
1602
            storage_buffer_align,
×
1603
            dispatch_indirect_aligned_size.get(),
×
1604
            render_effect_indirect_aligned_size.get(),
×
1605
            render_group_indirect_aligned_size.get()
×
1606
        );
1607

1608
        Self {
1609
            storage_buffer_align: NonZeroU32::new(storage_buffer_align).unwrap(),
2✔
1610
            dispatch_indirect_aligned_size,
1611
            render_effect_indirect_aligned_size,
1612
            render_group_indirect_aligned_size,
1613
            particle_group_aligned_size,
1614
        }
1615
    }
1616

1617
    /// Byte alignment for any storage buffer binding.
1618
    pub fn storage_buffer_align(&self) -> NonZeroU32 {
1✔
1619
        self.storage_buffer_align
1✔
1620
    }
1621

1622
    /// Byte alignment for [`GpuDispatchIndirect`].
1623
    pub fn dispatch_indirect_offset(&self, buffer_index: u32) -> u32 {
1✔
1624
        self.dispatch_indirect_aligned_size.get() * buffer_index
1✔
1625
    }
1626

1627
    /// Byte offset of the [`GpuRenderEffectMetadata`] of a given buffer.
1628
    pub fn render_effect_indirect_offset(&self, buffer_index: u32) -> u64 {
1✔
1629
        self.render_effect_indirect_aligned_size.get() as u64 * buffer_index as u64
1✔
1630
    }
1631

1632
    /// Byte alignment for [`GpuRenderEffectMetadata`].
1633
    pub fn render_effect_indirect_size(&self) -> NonZeroU64 {
×
1634
        NonZeroU64::new(self.render_effect_indirect_aligned_size.get() as u64).unwrap()
×
1635
    }
1636

1637
    /// Byte offset for the [`GpuRenderGroupIndirect`] of a given buffer.
1638
    pub fn render_group_indirect_offset(&self, buffer_index: u32) -> u64 {
1✔
1639
        self.render_group_indirect_aligned_size.get() as u64 * buffer_index as u64
1✔
1640
    }
1641

1642
    /// Byte alignment for [`GpuRenderGroupIndirect`].
1643
    pub fn render_group_indirect_size(&self) -> NonZeroU64 {
×
1644
        NonZeroU64::new(self.render_group_indirect_aligned_size.get() as u64).unwrap()
×
1645
    }
1646

1647
    /// Byte offset for the [`GpuParticleGroup`] of a given buffer.
1648
    pub fn particle_group_offset(&self, buffer_index: u32) -> u32 {
×
1649
        self.particle_group_aligned_size.get() * buffer_index
×
1650
    }
1651
}
1652

1653
struct CacheEntry {
1654
    cache_id: EffectCacheId,
1655
}
1656

1657
/// Global resource containing the GPU data to draw all the particle effects in
1658
/// all views.
1659
///
1660
/// The resource is populated by [`prepare_effects()`] with all the effects to
1661
/// render for the current frame, for all views in the frame, and consumed by
1662
/// [`queue_effects()`] to actually enqueue the drawning commands to draw those
1663
/// effects.
1664
#[derive(Resource)]
1665
pub struct EffectsMeta {
1666
    /// Map from an entity of the main world with a [`ParticleEffect`] component
1667
    /// attached to it, to the associated effect slice allocated in the
1668
    /// [`EffectCache`].
1669
    ///
1670
    /// [`ParticleEffect`]: crate::ParticleEffect
1671
    entity_map: HashMap<Entity, CacheEntry>,
1672
    /// Bind group for the camera view, containing the camera projection and
1673
    /// other uniform values related to the camera.
1674
    view_bind_group: Option<BindGroup>,
1675
    /// Bind group for the simulation parameters, like the current time and
1676
    /// frame delta time.
1677
    sim_params_bind_group: Option<BindGroup>,
1678
    /// Bind group for the spawning parameters (number of particles to spawn
1679
    /// this frame, ...).
1680
    spawner_bind_group: Option<BindGroup>,
1681
    /// Bind group #0 of the vfx_indirect shader, containing both the indirect
1682
    /// compute dispatch and render buffers.
1683
    dr_indirect_bind_group: Option<BindGroup>,
1684
    /// Bind group #3 of the vfx_init shader, containing the indirect render
1685
    /// buffer.
1686
    init_render_indirect_bind_group: Option<BindGroup>,
1687
    /// Global shared GPU uniform buffer storing the simulation parameters,
1688
    /// uploaded each frame from CPU to GPU.
1689
    sim_params_uniforms: UniformBuffer<GpuSimParams>,
1690
    /// Global shared GPU buffer storing the various spawner parameter structs
1691
    /// for the active effect instances.
1692
    spawner_buffer: AlignedBufferVec<GpuSpawnerParams>,
1693
    /// Global shared GPU buffer storing the various indirect dispatch structs
1694
    /// for the indirect dispatch of the Update pass.
1695
    dispatch_indirect_buffer: BufferTable<GpuDispatchIndirect>,
1696
    /// Global shared GPU buffer storing the various `RenderEffectMetadata`
1697
    /// structs for the active effect instances.
1698
    render_effect_dispatch_buffer: BufferTable<GpuRenderEffectMetadata>,
1699
    /// Stores the GPU `RenderGroupIndirect` structures, which describe mutable
1700
    /// data specific to a particle group.
1701
    ///
1702
    /// These structures also store the data needed for indirect dispatch of
1703
    /// drawcalls.
1704
    render_group_dispatch_buffer: BufferTable<GpuRenderGroupIndirect>,
1705
    /// Stores the GPU `ParticleGroup` structures, which are metadata describing
1706
    /// each particle group that's populated by the CPU and read (only read) by
1707
    /// the GPU.
1708
    particle_group_buffer: AlignedBufferVec<GpuParticleGroup>,
1709
    /// Unscaled vertices of the mesh of a single particle, generally a quad.
1710
    /// The mesh is later scaled during rendering by the "particle size".
1711
    // FIXME - This is a per-effect thing, unless we merge all meshes into a single buffer (makes
1712
    // sense) but in that case we need a vertex slice too to know which mesh to draw per effect.
1713
    vertices: BufferVec<GpuParticleVertex>,
1714
    /// Various GPU limits and aligned sizes lazily allocated and cached for
1715
    /// convenience.
1716
    gpu_limits: GpuLimits,
1717
}
1718

1719
impl EffectsMeta {
1720
    pub fn new(device: RenderDevice) -> Self {
1✔
1721
        let mut vertices = BufferVec::new(BufferUsages::VERTEX);
1✔
1722
        for v in QUAD_VERTEX_POSITIONS {
19✔
1723
            let uv = v.truncate() + 0.5;
6✔
1724
            let v = *v * Vec3::new(1.0, 1.0, 1.0);
6✔
1725
            vertices.push(GpuParticleVertex {
6✔
1726
                position: v.into(),
6✔
1727
                uv: uv.into(),
6✔
1728
            });
1729
        }
1730

1731
        let gpu_limits = GpuLimits::from_device(&device);
1✔
1732

1733
        // Ensure individual GpuSpawnerParams elements are properly aligned so they can
1734
        // be addressed individually by the computer shaders.
1735
        let item_align = gpu_limits.storage_buffer_align().get() as u64;
1✔
1736
        trace!(
1✔
1737
            "Aligning storage buffers to {} bytes as device limits requires.",
×
1738
            item_align
1739
        );
1740

1741
        Self {
1742
            entity_map: HashMap::default(),
1✔
1743
            view_bind_group: None,
1744
            sim_params_bind_group: None,
1745
            spawner_bind_group: None,
1746
            dr_indirect_bind_group: None,
1747
            init_render_indirect_bind_group: None,
1748
            sim_params_uniforms: UniformBuffer::default(),
1✔
1749
            spawner_buffer: AlignedBufferVec::new(
1✔
1750
                BufferUsages::STORAGE,
1751
                NonZeroU64::new(item_align),
1752
                Some("hanabi:buffer:spawner".to_string()),
1753
            ),
1754
            dispatch_indirect_buffer: BufferTable::new(
1✔
1755
                BufferUsages::STORAGE | BufferUsages::INDIRECT,
1756
                // NOTE: Technically we're using an offset in dispatch_workgroups_indirect(), but
1757
                // `min_storage_buffer_offset_alignment` is documented as being for the offset in
1758
                // BufferBinding and the dynamic offset in set_bind_group(), so either the
1759
                // documentation is lacking or we don't need to align here.
1760
                NonZeroU64::new(item_align),
1761
                Some("hanabi:buffer:dispatch_indirect".to_string()),
1762
            ),
1763
            render_effect_dispatch_buffer: BufferTable::new(
1✔
1764
                BufferUsages::STORAGE | BufferUsages::INDIRECT,
1765
                NonZeroU64::new(item_align),
1766
                Some("hanabi:buffer:render_effect_dispatch".to_string()),
1767
            ),
1768
            render_group_dispatch_buffer: BufferTable::new(
1✔
1769
                BufferUsages::STORAGE | BufferUsages::INDIRECT,
1770
                NonZeroU64::new(item_align),
1771
                Some("hanabi:buffer:render_group_dispatch".to_string()),
1772
            ),
1773
            particle_group_buffer: AlignedBufferVec::new(
1✔
1774
                BufferUsages::STORAGE,
1775
                NonZeroU64::new(item_align),
1776
                Some("hanabi:buffer:particle_group".to_string()),
1777
            ),
1778
            vertices,
1779
            gpu_limits,
1780
        }
1781
    }
1782

1783
    /// Allocate internal resources for newly spawned effects, and deallocate
1784
    /// them for just-removed ones.
1785
    pub fn add_remove_effects(
10✔
1786
        &mut self,
1787
        mut added_effects: Vec<AddedEffect>,
1788
        removed_effect_entities: Vec<Entity>,
1789
        render_device: &RenderDevice,
1790
        render_queue: &RenderQueue,
1791
        effect_bind_groups: &mut ResMut<EffectBindGroups>,
1792
        effect_cache: &mut ResMut<EffectCache>,
1793
    ) {
1794
        // Deallocate GPU data for destroyed effect instances. This will automatically
1795
        // drop any group where there is no more effect slice.
1796
        trace!(
10✔
1797
            "Removing {} despawned effects",
×
1798
            removed_effect_entities.len()
×
1799
        );
1800
        for entity in &removed_effect_entities {
10✔
1801
            trace!("Removing ParticleEffect on entity {:?}", entity);
×
1802
            if let Some(entry) = self.entity_map.remove(entity) {
×
1803
                trace!(
1804
                    "=> ParticleEffect on entity {:?} had cache ID {:?}, removing...",
×
1805
                    entity,
1806
                    entry.cache_id
1807
                );
1808
                if let Some(cached_effect_indices) = effect_cache.remove(entry.cache_id) {
×
1809
                    // Clear bind groups associated with the removed buffer
1810
                    trace!(
1811
                        "=> GPU buffer #{} gone, destroying its bind groups...",
×
1812
                        cached_effect_indices.buffer_index
1813
                    );
1814
                    effect_bind_groups
×
1815
                        .particle_buffers
×
1816
                        .remove(&cached_effect_indices.buffer_index);
×
1817

1818
                    let slices_ref = &cached_effect_indices.slices;
×
1819
                    debug_assert!(slices_ref.ranges.len() >= 2);
×
1820
                    let group_count = (slices_ref.ranges.len() - 1) as u32;
×
1821

1822
                    let first_row = slices_ref
×
1823
                        .dispatch_buffer_indices
×
1824
                        .first_update_group_dispatch_buffer_index
×
1825
                        .0;
×
1826
                    for table_id in first_row..(first_row + group_count) {
×
1827
                        self.dispatch_indirect_buffer
×
1828
                            .remove(BufferTableId(table_id));
×
1829
                    }
1830
                    self.render_effect_dispatch_buffer.remove(
×
1831
                        slices_ref
×
1832
                            .dispatch_buffer_indices
×
1833
                            .render_effect_metadata_buffer_index,
×
1834
                    );
1835
                    let first_row = slices_ref
×
1836
                        .dispatch_buffer_indices
×
1837
                        .first_render_group_dispatch_buffer_index
×
1838
                        .0;
×
1839
                    for table_id in first_row..(first_row + group_count) {
×
1840
                        self.render_group_dispatch_buffer
×
1841
                            .remove(BufferTableId(table_id));
×
1842
                    }
1843
                }
1844
            }
1845
        }
1846

1847
        // FIXME - We delete a buffer above, and have a chance to immediatly re-create
1848
        // it below. We should keep the GPU buffer around until the end of this method.
1849
        // On the other hand, we should also be careful that allocated buffers need to
1850
        // be tightly packed because 'vfx_indirect.wgsl' index them by buffer index in
1851
        // order, so doesn't support offset.
1852

1853
        trace!("Adding {} newly spawned effects", added_effects.len());
10✔
1854
        for added_effect in added_effects.drain(..) {
10✔
1855
            let first_update_group_dispatch_buffer_index = allocate_sequential_buffers(
1856
                &mut self.dispatch_indirect_buffer,
×
1857
                iter::repeat(GpuDispatchIndirect::default()).take(added_effect.groups.len()),
×
1858
            );
1859

1860
            let render_effect_dispatch_buffer_id = self
×
1861
                .render_effect_dispatch_buffer
×
1862
                .insert(GpuRenderEffectMetadata::default());
×
1863

1864
            let mut current_base_instance = 0;
×
1865
            let first_render_group_dispatch_buffer_index = allocate_sequential_buffers(
1866
                &mut self.render_group_dispatch_buffer,
×
1867
                added_effect.groups.iter().map(|group| {
×
1868
                    let indirect_dispatch = GpuRenderGroupIndirect {
×
1869
                        vertex_count: 6, // TODO - Flexible vertex count and mesh particles
×
1870
                        dead_count: group.capacity,
×
1871
                        base_instance: current_base_instance,
×
1872
                        max_spawn: group.capacity,
×
1873
                        ..default()
×
1874
                    };
1875
                    current_base_instance += group.capacity;
×
1876
                    indirect_dispatch
×
1877
                }),
1878
            );
1879

1880
            let mut trail_dispatch_buffer_indices = HashMap::new();
×
1881
            for (dest_group_index, group) in added_effect.groups.iter().enumerate() {
×
1882
                let Some(src_group_index) = group.src_group_index_if_trail else {
×
1883
                    continue;
×
1884
                };
1885
                trail_dispatch_buffer_indices.insert(
1886
                    dest_group_index as u32,
1887
                    TrailDispatchBufferIndices {
1888
                        dest: first_render_group_dispatch_buffer_index
1889
                            .offset(dest_group_index as u32),
1890
                        src: first_render_group_dispatch_buffer_index.offset(src_group_index),
1891
                    },
1892
                );
1893
            }
1894

1895
            let dispatch_buffer_indices = DispatchBufferIndices {
1896
                first_update_group_dispatch_buffer_index,
1897
                render_effect_metadata_buffer_index: render_effect_dispatch_buffer_id,
1898
                first_render_group_dispatch_buffer_index,
1899
                trail_dispatch_buffer_indices,
1900
            };
1901

1902
            // Insert the effect into the cache. This will allocate all the necessary GPU
1903
            // resources as needed.
1904
            let cache_id = effect_cache.insert(
×
1905
                added_effect.handle,
×
1906
                added_effect
×
1907
                    .groups
×
1908
                    .iter()
×
1909
                    .map(|group| group.capacity)
×
1910
                    .collect(),
×
1911
                &added_effect.particle_layout,
×
1912
                &added_effect.property_layout,
×
1913
                added_effect.layout_flags,
×
1914
                dispatch_buffer_indices,
×
1915
                added_effect.group_order,
×
1916
            );
1917

1918
            let entity = added_effect.entity;
×
1919
            self.entity_map.insert(entity, CacheEntry { cache_id });
×
1920

1921
            // Note: those effects are already in extracted_effects.effects
1922
            // because they were gathered by the same query as
1923
            // previously existing ones, during extraction.
1924

1925
            // let index = self.effect_cache.buffer_index(cache_id).unwrap();
1926
            //
1927
            // let table_id = self
1928
            // .dispatch_indirect_buffer
1929
            // .insert(GpuDispatchIndirect::default());
1930
            // assert_eq!(
1931
            // table_id.0, index,
1932
            // "Broken table invariant: buffer={} row={}",
1933
            // index, table_id.0
1934
            // );
1935
        }
1936

1937
        // Once all changes are applied, immediately schedule any GPU buffer
1938
        // (re)allocation based on the new buffer size. The actual GPU buffer content
1939
        // will be written later.
1940
        if self
10✔
1941
            .dispatch_indirect_buffer
10✔
1942
            .allocate_gpu(render_device, render_queue)
10✔
1943
        {
1944
            // All those bind groups use the buffer so need to be re-created
1945
            effect_bind_groups.particle_buffers.clear();
×
1946
        }
1947
        if self
10✔
1948
            .render_effect_dispatch_buffer
10✔
1949
            .allocate_gpu(render_device, render_queue)
10✔
1950
        {
1951
            // All those bind groups use the buffer so need to be re-created
1952
            self.dr_indirect_bind_group = None;
×
1953
            self.init_render_indirect_bind_group = None;
×
1954
            effect_bind_groups
×
1955
                .update_render_indirect_bind_groups
×
1956
                .clear();
1957
        }
1958
        if self
10✔
1959
            .render_group_dispatch_buffer
10✔
1960
            .allocate_gpu(render_device, render_queue)
10✔
1961
        {
1962
            // All those bind groups use the buffer so need to be re-created
1963
            self.dr_indirect_bind_group = None;
×
1964
            self.init_render_indirect_bind_group = None;
×
1965
            effect_bind_groups
×
1966
                .update_render_indirect_bind_groups
×
1967
                .clear();
1968
        }
1969
    }
1970
}
1971

1972
const QUAD_VERTEX_POSITIONS: &[Vec3] = &[
1973
    Vec3::from_array([-0.5, -0.5, 0.0]),
1974
    Vec3::from_array([0.5, 0.5, 0.0]),
1975
    Vec3::from_array([-0.5, 0.5, 0.0]),
1976
    Vec3::from_array([-0.5, -0.5, 0.0]),
1977
    Vec3::from_array([0.5, -0.5, 0.0]),
1978
    Vec3::from_array([0.5, 0.5, 0.0]),
1979
];
1980

1981
bitflags! {
1982
    /// Effect flags.
1983
    #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
1984
    pub struct LayoutFlags: u32 {
1985
        /// No flags.
1986
        const NONE = 0;
1987
        // DEPRECATED - The effect uses an image texture.
1988
        //const PARTICLE_TEXTURE = (1 << 0);
1989
        /// The effect is simulated in local space.
1990
        const LOCAL_SPACE_SIMULATION = (1 << 2);
1991
        /// The effect uses alpha masking instead of alpha blending. Only used for 3D.
1992
        const USE_ALPHA_MASK = (1 << 3);
1993
        /// The effect is rendered with flipbook texture animation based on the [`Attribute::SPRITE_INDEX`] of each particle.
1994
        const FLIPBOOK = (1 << 4);
1995
        /// The effect needs UVs.
1996
        const NEEDS_UV = (1 << 5);
1997
        /// The effect has ribbons.
1998
        const RIBBONS = (1 << 6);
1999
    }
2000
}
2001

2002
impl Default for LayoutFlags {
2003
    fn default() -> Self {
1✔
2004
        Self::NONE
1✔
2005
    }
2006
}
2007

2008
pub(crate) fn prepare_effects(
10✔
2009
    mut commands: Commands,
2010
    sim_params: Res<SimParams>,
2011
    render_device: Res<RenderDevice>,
2012
    render_queue: Res<RenderQueue>,
2013
    pipeline_cache: Res<PipelineCache>,
2014
    init_pipeline: Res<ParticlesInitPipeline>,
2015
    update_pipeline: Res<ParticlesUpdatePipeline>,
2016
    mut specialized_init_pipelines: ResMut<SpecializedComputePipelines<ParticlesInitPipeline>>,
2017
    mut specialized_update_pipelines: ResMut<SpecializedComputePipelines<ParticlesUpdatePipeline>>,
2018
    mut effects_meta: ResMut<EffectsMeta>,
2019
    mut effect_cache: ResMut<EffectCache>,
2020
    mut extracted_effects: ResMut<ExtractedEffects>,
2021
    mut effect_bind_groups: ResMut<EffectBindGroups>,
2022
) {
2023
    trace!("prepare_effects");
10✔
2024

2025
    // Allocate spawner buffer if needed
2026
    // if effects_meta.spawner_buffer.is_empty() {
2027
    //    effects_meta.spawner_buffer.push(GpuSpawnerParams::default());
2028
    //}
2029

2030
    // Write vertices (TODO - lazily once only)
2031
    effects_meta
10✔
2032
        .vertices
10✔
2033
        .write_buffer(&render_device, &render_queue);
10✔
2034

2035
    // Clear last frame's buffer resizes which may have occured during last frame,
2036
    // during `Node::run()` while the `BufferTable` could not be mutated.
2037
    effects_meta
10✔
2038
        .dispatch_indirect_buffer
10✔
2039
        .clear_previous_frame_resizes();
2040
    effects_meta
10✔
2041
        .render_effect_dispatch_buffer
10✔
2042
        .clear_previous_frame_resizes();
2043
    effects_meta
10✔
2044
        .render_group_dispatch_buffer
10✔
2045
        .clear_previous_frame_resizes();
2046

2047
    // Allocate new effects, deallocate removed ones
2048
    let removed_effect_entities = std::mem::take(&mut extracted_effects.removed_effect_entities);
10✔
2049
    for entity in &removed_effect_entities {
10✔
2050
        extracted_effects.effects.remove(entity);
×
2051
    }
2052
    effects_meta.add_remove_effects(
2053
        std::mem::take(&mut extracted_effects.added_effects),
2054
        removed_effect_entities,
2055
        &render_device,
2056
        &render_queue,
2057
        &mut effect_bind_groups,
2058
        &mut effect_cache,
2059
    );
2060

2061
    // // sort first by z and then by handle. this ensures that, when possible,
2062
    // batches span multiple z layers // batches won't span z-layers if there is
2063
    // another batch between them extracted_effects.effects.sort_by(|a, b| {
2064
    //     match FloatOrd(a.transform.w_axis[2]).cmp(&FloatOrd(b.transform.
2065
    // w_axis[2])) {         Ordering::Equal => a.handle.cmp(&b.handle),
2066
    //         other => other,
2067
    //     }
2068
    // });
2069

2070
    // Build batcher inputs from extracted effects
2071
    let effects = std::mem::take(&mut extracted_effects.effects);
2072

2073
    let effect_entity_list = effects
2074
        .into_iter()
2075
        .map(|(entity, extracted_effect)| {
×
2076
            let id = effects_meta.entity_map.get(&entity).unwrap().cache_id;
×
2077
            let property_buffer = effect_cache.get_property_buffer(id).cloned(); // clone handle for lifetime
×
2078
            let effect_slices = effect_cache.get_slices(id);
×
2079
            let group_order = effect_cache.get_group_order(id);
×
2080

2081
            BatchesInput {
×
2082
                handle: extracted_effect.handle,
×
2083
                entity,
×
2084
                effect_slices,
×
2085
                property_layout: extracted_effect.property_layout.clone(),
×
2086
                effect_shaders: extracted_effect.effect_shaders.clone(),
×
2087
                layout_flags: extracted_effect.layout_flags,
×
2088
                texture_layout: extracted_effect.texture_layout.clone(),
×
2089
                textures: extracted_effect.textures.clone(),
×
2090
                alpha_mode: extracted_effect.alpha_mode,
×
2091
                transform: extracted_effect.transform.into(),
×
2092
                inverse_transform: extracted_effect.inverse_transform.into(),
×
2093
                particle_layout: extracted_effect.particle_layout.clone(),
×
2094
                property_buffer,
×
2095
                group_order: group_order.to_vec(),
×
2096
                property_data: extracted_effect.property_data,
×
2097
                initializers: extracted_effect.initializers,
×
2098
                #[cfg(feature = "2d")]
×
2099
                z_sort_key_2d: extracted_effect.z_sort_key_2d,
×
2100
            }
2101
        })
2102
        .collect::<Vec<_>>();
2103
    trace!("Collected {} extracted effects", effect_entity_list.len());
×
2104

2105
    // Sort first by effect buffer index, then by slice range (see EffectSlice)
2106
    // inside that buffer. This is critical for batching to work, because
2107
    // batching effects is based on compatible items, which implies same GPU
2108
    // buffer and continuous slice ranges (the next slice start must be equal to
2109
    // the previous start end, without gap). EffectSlice already contains both
2110
    // information, and the proper ordering implementation.
2111
    // effect_entity_list.sort_by_key(|a| a.effect_slice.clone());
2112

2113
    // Loop on all extracted effects in order, and try to batch them together to
2114
    // reduce draw calls.
2115
    effects_meta.spawner_buffer.clear();
10✔
2116
    effects_meta.particle_group_buffer.clear();
10✔
2117
    let mut total_group_count = 0;
10✔
2118
    for (effect_index, input) in effect_entity_list.into_iter().enumerate() {
×
2119
        let particle_layout_min_binding_size =
×
2120
            input.effect_slices.particle_layout.min_binding_size();
×
2121
        let property_layout_min_binding_size = if input.property_layout.is_empty() {
×
2122
            None
×
2123
        } else {
2124
            Some(input.property_layout.min_binding_size())
×
2125
        };
2126

2127
        // Create init pipeline key flags.
2128
        let mut init_pipeline_key_flags = ParticleInitPipelineKeyFlags::empty();
×
2129
        init_pipeline_key_flags.set(
×
2130
            ParticleInitPipelineKeyFlags::ATTRIBUTE_PREV,
×
2131
            input.particle_layout.contains(Attribute::PREV),
×
2132
        );
2133
        init_pipeline_key_flags.set(
×
2134
            ParticleInitPipelineKeyFlags::ATTRIBUTE_NEXT,
×
2135
            input.particle_layout.contains(Attribute::NEXT),
×
2136
        );
2137

2138
        // Specialize the init pipeline based on the effect.
2139
        let init_and_update_pipeline_ids = input
×
2140
            .effect_shaders
×
2141
            .iter()
2142
            .enumerate()
2143
            .map(|(group_index, shader)| {
×
2144
                let mut flags = init_pipeline_key_flags;
×
2145

2146
                // If this is a cloner, add the appropriate flag.
2147
                match input.initializers[group_index] {
×
2148
                    EffectInitializer::Spawner(_) => {}
×
2149
                    EffectInitializer::Cloner(_) => {
×
2150
                        flags.insert(ParticleInitPipelineKeyFlags::CLONE);
×
2151
                    }
2152
                }
2153

2154
                let init_pipeline_id = specialized_init_pipelines.specialize(
×
2155
                    &pipeline_cache,
×
2156
                    &init_pipeline,
×
2157
                    ParticleInitPipelineKey {
×
2158
                        shader: shader.init.clone(),
×
2159
                        particle_layout_min_binding_size,
×
2160
                        property_layout_min_binding_size,
×
2161
                        flags,
×
2162
                    },
2163
                );
2164
                trace!("Init pipeline specialized: id={:?}", init_pipeline_id);
×
2165

2166
                let update_pipeline_id = specialized_update_pipelines.specialize(
×
2167
                    &pipeline_cache,
2168
                    &update_pipeline,
2169
                    ParticleUpdatePipelineKey {
2170
                        shader: shader.update.clone(),
2171
                        particle_layout: input.effect_slices.particle_layout.clone(),
2172
                        property_layout: input.property_layout.clone(),
2173
                        is_trail: matches!(
×
2174
                            input.initializers[group_index],
2175
                            EffectInitializer::Cloner(_)
2176
                        ),
2177
                    },
2178
                );
2179
                trace!("Update pipeline specialized: id={:?}", update_pipeline_id);
×
2180

2181
                InitAndUpdatePipelineIds {
×
2182
                    init: init_pipeline_id,
×
2183
                    update: update_pipeline_id,
×
2184
                }
2185
            })
2186
            .collect();
2187

2188
        let init_shaders: Vec<_> = input
×
2189
            .effect_shaders
×
2190
            .iter()
2191
            .map(|shaders| shaders.init.clone())
×
2192
            .collect();
2193
        trace!("init_shader(s) = {:?}", init_shaders);
×
2194

2195
        let update_shaders: Vec<_> = input
×
2196
            .effect_shaders
×
2197
            .iter()
2198
            .map(|shaders| shaders.update.clone())
×
2199
            .collect();
2200
        trace!("update_shader(s) = {:?}", update_shaders);
×
2201

2202
        let render_shaders: Vec<_> = input
×
2203
            .effect_shaders
×
2204
            .iter()
2205
            .map(|shaders| shaders.render.clone())
×
2206
            .collect();
2207
        trace!("render_shader(s) = {:?}", render_shaders);
×
2208

2209
        let layout_flags = input.layout_flags;
×
2210
        trace!("layout_flags = {:?}", layout_flags);
×
2211

2212
        trace!(
×
2213
            "particle_layout = {:?}",
×
2214
            input.effect_slices.particle_layout
2215
        );
2216

2217
        #[cfg(feature = "2d")]
2218
        {
2219
            trace!("z_sort_key_2d = {:?}", input.z_sort_key_2d);
×
2220
        }
2221

2222
        // This callback is raised when creating a new batch from a single item, so the
2223
        // base index for spawners is the current buffer size. Per-effect spawner values
2224
        // will be pushed in order into the array.
2225
        let spawner_base = effects_meta.spawner_buffer.len() as u32;
×
2226

2227
        for initializer in input.initializers.iter() {
×
2228
            match initializer {
×
2229
                EffectInitializer::Spawner(effect_spawner) => {
×
2230
                    let spawner_params = GpuSpawnerParams {
2231
                        transform: input.transform,
×
2232
                        inverse_transform: input.inverse_transform,
×
2233
                        spawn: effect_spawner.spawn_count as i32,
×
2234
                        // FIXME - Probably bad to re-seed each time there's a change
2235
                        seed: random::<u32>(),
×
2236
                        count: 0,
2237
                        // FIXME: the effect_index is global inside the global spawner buffer,
2238
                        // but the group_index is the index of the particle buffer, which can
2239
                        // in theory (with batching) contain > 1 effect per buffer.
2240
                        effect_index: input.effect_slices.buffer_index,
×
2241
                        lifetime: 0.0,
2242
                        pad: Default::default(),
×
2243
                    };
2244
                    trace!("spawner params = {:?}", spawner_params);
×
2245
                    effects_meta.spawner_buffer.push(spawner_params);
×
2246
                }
2247

2248
                EffectInitializer::Cloner(effect_cloner) => {
×
2249
                    let spawner_params = GpuSpawnerParams {
2250
                        transform: input.transform,
×
2251
                        inverse_transform: input.inverse_transform,
×
2252
                        spawn: 0,
2253
                        // FIXME - Probably bad to re-seed each time there's a change
2254
                        seed: random::<u32>(),
×
2255
                        count: 0,
2256
                        // FIXME: the effect_index is global inside the global spawner buffer,
2257
                        // but the group_index is the index of the particle buffer, which can
2258
                        // in theory (with batching) contain > 1 effect per buffer.
2259
                        effect_index: input.effect_slices.buffer_index,
×
2260
                        lifetime: effect_cloner.cloner.lifetime,
×
2261
                        pad: Default::default(),
×
2262
                    };
2263
                    trace!("cloner params = {:?}", spawner_params);
×
2264
                    effects_meta.spawner_buffer.push(spawner_params);
×
2265
                }
2266
            }
2267
        }
2268

2269
        // Create the particle group buffer entries.
2270
        let mut first_particle_group_buffer_index = None;
×
2271
        let mut local_group_count = 0;
×
2272
        for (group_index, range) in input.effect_slices.slices.windows(2).enumerate() {
×
2273
            let particle_group_buffer_index =
×
2274
                effects_meta.particle_group_buffer.push(GpuParticleGroup {
×
2275
                    global_group_index: total_group_count,
×
2276
                    effect_index: effect_index as u32,
×
2277
                    group_index_in_effect: group_index as u32,
×
2278
                    indirect_index: range[0],
×
2279
                    capacity: range[1] - range[0],
×
2280
                    effect_particle_offset: input.effect_slices.slices[0],
×
2281
                });
2282
            if group_index == 0 {
×
2283
                first_particle_group_buffer_index = Some(particle_group_buffer_index as u32);
×
2284
            }
2285
            total_group_count += 1;
×
2286
            local_group_count += 1;
×
2287
        }
2288

2289
        let effect_cache_id = effects_meta.entity_map.get(&input.entity).unwrap().cache_id;
×
2290
        let dispatch_buffer_indices = effect_cache
×
2291
            .get_dispatch_buffer_indices(effect_cache_id)
×
2292
            .clone();
2293

2294
        // Write properties for this effect if they were modified.
2295
        // FIXME - This doesn't work with batching!
2296
        if let Some(property_data) = &input.property_data {
×
2297
            trace!("Properties changed, need to (re-)upload to GPU");
×
2298
            if let Some(property_buffer) = input.property_buffer.as_ref() {
×
2299
                trace!("Scheduled property upload to GPU");
×
2300
                render_queue.write_buffer(property_buffer, 0, property_data);
×
2301
            } else {
2302
                error!("Cannot upload properties to GPU, no property buffer!");
×
2303
            }
2304
        }
2305

2306
        #[cfg(feature = "2d")]
2307
        let z_sort_key_2d = input.z_sort_key_2d;
×
2308

2309
        #[cfg(feature = "3d")]
2310
        let translation_3d = input.transform.translation();
×
2311

2312
        // Spawn one shared EffectBatches for all groups of this effect. This contains
2313
        // most of the data needed to drive rendering, except the per-group data.
2314
        // However this doesn't drive rendering; this is just storage.
2315
        let batches = EffectBatches::from_input(
2316
            input,
×
2317
            spawner_base,
×
2318
            effect_cache_id,
×
2319
            init_and_update_pipeline_ids,
×
2320
            dispatch_buffer_indices,
×
2321
            first_particle_group_buffer_index.unwrap_or_default(),
×
2322
        );
2323
        let batches_entity = commands.spawn(batches).id();
×
2324

2325
        // Spawn one EffectDrawBatch per group, to actually drive rendering. Each group
2326
        // renders with a different indirect call. These are the entities that the
2327
        // render phase items will receive.
2328
        for group_index in 0..local_group_count {
×
2329
            commands.spawn(EffectDrawBatch {
×
2330
                batches_entity,
×
2331
                group_index,
×
2332
                #[cfg(feature = "2d")]
×
2333
                z_sort_key_2d,
×
2334
                #[cfg(feature = "3d")]
×
2335
                translation_3d,
×
2336
            });
2337
        }
2338
    }
2339

2340
    // Write the entire spawner buffer for this frame, for all effects combined
2341
    effects_meta
10✔
2342
        .spawner_buffer
10✔
2343
        .write_buffer(&render_device, &render_queue);
10✔
2344

2345
    // Write the entire particle group buffer for this frame
2346
    if effects_meta
10✔
2347
        .particle_group_buffer
10✔
2348
        .write_buffer(&render_device, &render_queue)
10✔
2349
    {
2350
        // The buffer changed; invalidate all bind groups for all effects.
2351
    }
2352

2353
    // Update simulation parameters
2354
    effects_meta
10✔
2355
        .sim_params_uniforms
10✔
2356
        .set(sim_params.deref().into());
10✔
2357
    {
2358
        let gpu_sim_params = effects_meta.sim_params_uniforms.get_mut();
10✔
2359
        gpu_sim_params.num_groups = total_group_count;
10✔
2360

2361
        trace!(
10✔
2362
            "Simulation parameters: time={} delta_time={} virtual_time={} \
×
2363
                virtual_delta_time={} real_time={} real_delta_time={} num_groups={}",
×
2364
            gpu_sim_params.time,
2365
            gpu_sim_params.delta_time,
2366
            gpu_sim_params.virtual_time,
2367
            gpu_sim_params.virtual_delta_time,
2368
            gpu_sim_params.real_time,
2369
            gpu_sim_params.real_delta_time,
2370
            gpu_sim_params.num_groups,
2371
        );
2372
    }
2373
    let prev_buffer_id = effects_meta.sim_params_uniforms.buffer().map(|b| b.id());
19✔
2374
    effects_meta
2375
        .sim_params_uniforms
2376
        .write_buffer(&render_device, &render_queue);
2377
    if prev_buffer_id != effects_meta.sim_params_uniforms.buffer().map(|b| b.id()) {
11✔
2378
        // Buffer changed, invalidate bind groups
2379
        effects_meta.sim_params_bind_group = None;
1✔
2380
    }
2381
}
2382

2383
/// Per-buffer bind groups for a GPU effect buffer.
2384
///
2385
/// This contains all bind groups specific to a single [`EffectBuffer`].
2386
///
2387
/// [`EffectBuffer`]: crate::render::effect_cache::EffectBuffer
2388
pub(crate) struct BufferBindGroups {
2389
    /// Bind group for the render graphic shader.
2390
    ///
2391
    /// ```wgsl
2392
    /// @binding(0) var<storage, read> particle_buffer : ParticleBuffer;
2393
    /// @binding(1) var<storage, read> indirect_buffer : IndirectBuffer;
2394
    /// @binding(2) var<storage, read> dispatch_indirect : DispatchIndirect;
2395
    /// #ifdef RENDER_NEEDS_SPAWNER
2396
    /// @binding(3) var<storage, read> spawner : Spawner;
2397
    /// #endif
2398
    /// ```
2399
    render: BindGroup,
2400
}
2401

2402
/// Combination of a texture layout and the bound textures.
2403
#[derive(Debug, Default, Clone, PartialEq, Eq, Hash)]
2404
struct Material {
2405
    layout: TextureLayout,
2406
    textures: Vec<AssetId<Image>>,
2407
}
2408

2409
impl Material {
2410
    /// Get the bind group entries to create a bind group.
2411
    pub fn make_entries<'a>(
×
2412
        &self,
2413
        gpu_images: &'a RenderAssets<GpuImage>,
2414
    ) -> Vec<BindGroupEntry<'a>> {
2415
        self.textures
×
2416
            .iter()
2417
            .enumerate()
2418
            .flat_map(|(index, id)| {
×
NEW
2419
                let base_binding = index as u32 * 2;
×
2420
                if let Some(gpu_image) = gpu_images.get(*id) {
×
2421
                    vec![
×
2422
                        BindGroupEntry {
×
NEW
2423
                            binding: base_binding,
×
2424
                            resource: BindingResource::TextureView(&gpu_image.texture_view),
×
2425
                        },
2426
                        BindGroupEntry {
×
NEW
2427
                            binding: base_binding + 1,
×
2428
                            resource: BindingResource::Sampler(&gpu_image.sampler),
×
2429
                        },
2430
                    ]
2431
                } else {
2432
                    vec![]
×
2433
                }
2434
            })
2435
            .collect()
2436
    }
2437
}
2438

2439
#[derive(Default, Resource)]
2440
pub struct EffectBindGroups {
2441
    /// Map from buffer index to the bind groups shared among all effects that
2442
    /// use that buffer.
2443
    particle_buffers: HashMap<u32, BufferBindGroups>,
2444
    /// Map of bind groups for image assets used as particle textures.
2445
    images: HashMap<AssetId<Image>, BindGroup>,
2446
    /// Map from effect index to its update render indirect bind group (group
2447
    /// 3).
2448
    update_render_indirect_bind_groups: HashMap<EffectCacheId, BindGroup>,
2449
    /// Map from an effect material to its bind group.
2450
    material_bind_groups: HashMap<Material, BindGroup>,
2451
}
2452

2453
impl EffectBindGroups {
2454
    pub fn particle_render(&self, buffer_index: u32) -> Option<&BindGroup> {
×
2455
        self.particle_buffers
×
2456
            .get(&buffer_index)
×
2457
            .map(|bg| &bg.render)
×
2458
    }
2459
}
2460

2461
#[derive(SystemParam)]
2462
pub struct QueueEffectsReadOnlyParams<'w, 's> {
2463
    #[cfg(feature = "2d")]
2464
    draw_functions_2d: Res<'w, DrawFunctions<Transparent2d>>,
2465
    #[cfg(feature = "3d")]
2466
    draw_functions_3d: Res<'w, DrawFunctions<Transparent3d>>,
2467
    #[cfg(feature = "3d")]
2468
    draw_functions_alpha_mask: Res<'w, DrawFunctions<AlphaMask3d>>,
2469
    #[system_param(ignore)]
2470
    marker: PhantomData<&'s usize>,
2471
}
2472

2473
fn emit_sorted_draw<T, F>(
×
2474
    views: &Query<(Entity, &VisibleEntities, &ExtractedView)>,
2475
    render_phases: &mut ResMut<ViewSortedRenderPhases<T>>,
2476
    view_entities: &mut FixedBitSet,
2477
    effect_batches: &Query<(Entity, &mut EffectBatches)>,
2478
    effect_draw_batches: &Query<(Entity, &mut EffectDrawBatch)>,
2479
    render_pipeline: &mut ParticlesRenderPipeline,
2480
    mut specialized_render_pipelines: Mut<SpecializedRenderPipelines<ParticlesRenderPipeline>>,
2481
    pipeline_cache: &PipelineCache,
2482
    msaa_samples: u32,
2483
    make_phase_item: F,
2484
    #[cfg(all(feature = "2d", feature = "3d"))] pipeline_mode: PipelineMode,
2485
) where
2486
    T: SortedPhaseItem,
2487
    F: Fn(CachedRenderPipelineId, Entity, &EffectDrawBatch, u32, &ExtractedView) -> T,
2488
{
2489
    trace!("emit_sorted_draw() {} views", views.iter().len());
×
2490

2491
    for (view_entity, visible_entities, view) in views.iter() {
×
2492
        trace!("Process new sorted view");
×
2493

2494
        let Some(render_phase) = render_phases.get_mut(&view_entity) else {
×
2495
            continue;
×
2496
        };
2497

2498
        {
2499
            #[cfg(feature = "trace")]
2500
            let _span = bevy::utils::tracing::info_span!("collect_view_entities").entered();
×
2501

2502
            view_entities.clear();
×
2503
            view_entities.extend(
×
2504
                visible_entities
×
2505
                    .iter::<WithCompiledParticleEffect>()
×
2506
                    .map(|e| e.index() as usize),
×
2507
            );
2508
        }
2509

2510
        // For each view, loop over all the effect batches to determine if the effect
2511
        // needs to be rendered for that view, and enqueue a view-dependent
2512
        // batch if so.
2513
        for (draw_entity, draw_batch) in effect_draw_batches.iter() {
×
2514
            #[cfg(feature = "trace")]
2515
            let _span_draw = bevy::utils::tracing::info_span!("draw_batch").entered();
×
2516

2517
            trace!(
×
2518
                "Process draw batch: draw_entity={:?} group_index={} batches_entity={:?}",
×
2519
                draw_entity,
×
2520
                draw_batch.group_index,
×
2521
                draw_batch.batches_entity,
×
2522
            );
2523

2524
            // Get the EffectBatches this EffectDrawBatch is part of.
2525
            let Ok((batches_entity, batches)) = effect_batches.get(draw_batch.batches_entity)
×
2526
            else {
×
2527
                continue;
×
2528
            };
2529

2530
            trace!(
×
2531
                "-> EffectBaches: entity={:?} buffer_index={} spawner_base={} layout_flags={:?}",
×
2532
                batches_entity,
×
2533
                batches.buffer_index,
×
2534
                batches.spawner_base,
×
2535
                batches.layout_flags,
×
2536
            );
2537

2538
            // AlphaMask is a binned draw, so no sorted draw can possibly use it
2539
            if batches.layout_flags.contains(LayoutFlags::USE_ALPHA_MASK) {
×
2540
                continue;
×
2541
            }
2542

2543
            // Check if batch contains any entity visible in the current view. Otherwise we
2544
            // can skip the entire batch. Note: This is O(n^2) but (unlike
2545
            // the Sprite renderer this is inspired from) we don't expect more than
2546
            // a handful of particle effect instances, so would rather not pay the memory
2547
            // cost of a FixedBitSet for the sake of an arguable speed-up.
2548
            // TODO - Profile to confirm.
2549
            #[cfg(feature = "trace")]
2550
            let _span_check_vis = bevy::utils::tracing::info_span!("check_visibility").entered();
×
2551
            let has_visible_entity = batches
×
2552
                .entities
×
2553
                .iter()
2554
                .any(|index| view_entities.contains(*index as usize));
×
2555
            if !has_visible_entity {
×
2556
                trace!("No visible entity for view, not emitting any draw call.");
×
2557
                continue;
×
2558
            }
2559
            #[cfg(feature = "trace")]
2560
            _span_check_vis.exit();
×
2561

2562
            // Create and cache the bind group layout for this texture layout
2563
            render_pipeline.cache_material(&batches.texture_layout);
×
2564

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

2568
            let local_space_simulation = batches
×
2569
                .layout_flags
×
2570
                .contains(LayoutFlags::LOCAL_SPACE_SIMULATION);
×
2571
            let use_alpha_mask = batches.layout_flags.contains(LayoutFlags::USE_ALPHA_MASK);
×
2572
            let flipbook = batches.layout_flags.contains(LayoutFlags::FLIPBOOK);
×
2573
            let needs_uv = batches.layout_flags.contains(LayoutFlags::NEEDS_UV);
×
2574
            let ribbons = batches.layout_flags.contains(LayoutFlags::RIBBONS);
×
2575
            let image_count = batches.texture_layout.layout.len() as u8;
×
2576

2577
            // Specialize the render pipeline based on the effect batch
2578
            trace!(
×
2579
                "Specializing render pipeline: render_shaders={:?} image_count={} use_alpha_mask={:?} flipbook={:?} hdr={}",
×
2580
                batches.render_shaders,
×
2581
                image_count,
×
2582
                use_alpha_mask,
×
2583
                flipbook,
×
2584
                view.hdr
×
2585
            );
2586

2587
            // Add a draw pass for the effect batch
2588
            trace!("Emitting individual draws for batches and groups: group_batches.len()={} batches.render_shaders.len()={}", batches.group_batches.len(), batches.render_shaders.len());
×
2589
            let render_shader_source = &batches.render_shaders[draw_batch.group_index as usize];
×
2590
            trace!("Emit for group index #{}", draw_batch.group_index);
×
2591

2592
            let alpha_mode = batches.alpha_mode;
×
2593

2594
            #[cfg(feature = "trace")]
2595
            let _span_specialize = bevy::utils::tracing::info_span!("specialize").entered();
×
2596
            let render_pipeline_id = specialized_render_pipelines.specialize(
×
2597
                pipeline_cache,
×
2598
                render_pipeline,
×
2599
                ParticleRenderPipelineKey {
×
2600
                    shader: render_shader_source.clone(),
×
2601
                    particle_layout: batches.particle_layout.clone(),
×
2602
                    texture_layout: batches.texture_layout.clone(),
×
2603
                    local_space_simulation,
×
2604
                    use_alpha_mask,
×
2605
                    alpha_mode,
×
2606
                    flipbook,
×
2607
                    needs_uv,
×
2608
                    ribbons,
×
2609
                    #[cfg(all(feature = "2d", feature = "3d"))]
×
2610
                    pipeline_mode,
×
2611
                    msaa_samples,
×
2612
                    hdr: view.hdr,
×
2613
                },
2614
            );
2615
            #[cfg(feature = "trace")]
2616
            _span_specialize.exit();
×
2617

2618
            trace!(
×
2619
                "+ Render pipeline specialized: id={:?} -> group_index={}",
×
2620
                render_pipeline_id,
×
2621
                draw_batch.group_index
×
2622
            );
2623
            trace!(
×
2624
                "+ Add Transparent for batch on draw_entity {:?}: buffer_index={} \
×
2625
                group_index={} spawner_base={} handle={:?}",
×
2626
                draw_entity,
×
2627
                batches.buffer_index,
×
2628
                draw_batch.group_index,
×
2629
                batches.spawner_base,
×
2630
                batches.handle
×
2631
            );
2632
            render_phase.add(make_phase_item(
×
2633
                render_pipeline_id,
×
2634
                draw_entity,
×
2635
                draw_batch,
×
2636
                draw_batch.group_index,
×
2637
                view,
×
2638
            ));
2639
        }
2640
    }
2641
}
2642

2643
#[cfg(feature = "3d")]
2644
fn emit_binned_draw<T, F>(
×
2645
    views: &Query<(Entity, &VisibleEntities, &ExtractedView)>,
2646
    render_phases: &mut ResMut<ViewBinnedRenderPhases<T>>,
2647
    view_entities: &mut FixedBitSet,
2648
    effect_batches: &Query<(Entity, &mut EffectBatches)>,
2649
    effect_draw_batches: &Query<(Entity, &mut EffectDrawBatch)>,
2650
    render_pipeline: &mut ParticlesRenderPipeline,
2651
    mut specialized_render_pipelines: Mut<SpecializedRenderPipelines<ParticlesRenderPipeline>>,
2652
    pipeline_cache: &PipelineCache,
2653
    msaa_samples: u32,
2654
    make_bin_key: F,
2655
    #[cfg(all(feature = "2d", feature = "3d"))] pipeline_mode: PipelineMode,
2656
    use_alpha_mask: bool,
2657
) where
2658
    T: BinnedPhaseItem,
2659
    F: Fn(CachedRenderPipelineId, &EffectDrawBatch, u32, &ExtractedView) -> T::BinKey,
2660
{
2661
    use bevy::render::render_phase::BinnedRenderPhaseType;
2662

2663
    trace!("emit_binned_draw() {} views", views.iter().len());
×
2664

2665
    for (view_entity, visible_entities, view) in views.iter() {
×
2666
        trace!(
×
2667
            "Process new binned view (use_alpha_mask={})",
×
2668
            use_alpha_mask
×
2669
        );
2670

2671
        let Some(render_phase) = render_phases.get_mut(&view_entity) else {
×
2672
            continue;
×
2673
        };
2674

2675
        {
2676
            #[cfg(feature = "trace")]
2677
            let _span = bevy::utils::tracing::info_span!("collect_view_entities").entered();
×
2678

2679
            view_entities.clear();
×
2680
            view_entities.extend(
×
2681
                visible_entities
×
2682
                    .iter::<WithCompiledParticleEffect>()
×
2683
                    .map(|e| e.index() as usize),
×
2684
            );
2685
        }
2686

2687
        // For each view, loop over all the effect batches to determine if the effect
2688
        // needs to be rendered for that view, and enqueue a view-dependent
2689
        // batch if so.
2690
        for (draw_entity, draw_batch) in effect_draw_batches.iter() {
×
2691
            #[cfg(feature = "trace")]
2692
            let _span_draw = bevy::utils::tracing::info_span!("draw_batch").entered();
×
2693

2694
            trace!(
×
2695
                "Process draw batch: draw_entity={:?} group_index={} batches_entity={:?}",
×
2696
                draw_entity,
×
2697
                draw_batch.group_index,
×
2698
                draw_batch.batches_entity,
×
2699
            );
2700

2701
            // Get the EffectBatches this EffectDrawBatch is part of.
2702
            let Ok((batches_entity, batches)) = effect_batches.get(draw_batch.batches_entity)
×
2703
            else {
×
2704
                continue;
×
2705
            };
2706

2707
            trace!(
×
2708
                "-> EffectBaches: entity={:?} buffer_index={} spawner_base={} layout_flags={:?}",
×
2709
                batches_entity,
×
2710
                batches.buffer_index,
×
2711
                batches.spawner_base,
×
2712
                batches.layout_flags,
×
2713
            );
2714

2715
            if use_alpha_mask != batches.layout_flags.contains(LayoutFlags::USE_ALPHA_MASK) {
×
2716
                continue;
×
2717
            }
2718

2719
            // Check if batch contains any entity visible in the current view. Otherwise we
2720
            // can skip the entire batch. Note: This is O(n^2) but (unlike
2721
            // the Sprite renderer this is inspired from) we don't expect more than
2722
            // a handful of particle effect instances, so would rather not pay the memory
2723
            // cost of a FixedBitSet for the sake of an arguable speed-up.
2724
            // TODO - Profile to confirm.
2725
            #[cfg(feature = "trace")]
2726
            let _span_check_vis = bevy::utils::tracing::info_span!("check_visibility").entered();
×
2727
            let has_visible_entity = batches
×
2728
                .entities
×
2729
                .iter()
2730
                .any(|index| view_entities.contains(*index as usize));
×
2731
            if !has_visible_entity {
×
2732
                trace!("No visible entity for view, not emitting any draw call.");
×
2733
                continue;
×
2734
            }
2735
            #[cfg(feature = "trace")]
2736
            _span_check_vis.exit();
×
2737

2738
            // Create and cache the bind group layout for this texture layout
2739
            render_pipeline.cache_material(&batches.texture_layout);
×
2740

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

2744
            let local_space_simulation = batches
×
2745
                .layout_flags
×
2746
                .contains(LayoutFlags::LOCAL_SPACE_SIMULATION);
×
2747
            let use_alpha_mask = batches.layout_flags.contains(LayoutFlags::USE_ALPHA_MASK);
×
2748
            let flipbook = batches.layout_flags.contains(LayoutFlags::FLIPBOOK);
×
2749
            let needs_uv = batches.layout_flags.contains(LayoutFlags::NEEDS_UV);
×
2750
            let ribbons = batches.layout_flags.contains(LayoutFlags::RIBBONS);
×
2751
            let image_count = batches.texture_layout.layout.len() as u8;
×
2752

2753
            // Specialize the render pipeline based on the effect batch
2754
            trace!(
×
2755
                "Specializing render pipeline: render_shaders={:?} image_count={} use_alpha_mask={:?} flipbook={:?} hdr={}",
×
2756
                batches.render_shaders,
×
2757
                image_count,
×
2758
                use_alpha_mask,
×
2759
                flipbook,
×
2760
                view.hdr
×
2761
            );
2762

2763
            // Add a draw pass for the effect batch
2764
            trace!("Emitting individual draws for batches and groups: group_batches.len()={} batches.render_shaders.len()={}", batches.group_batches.len(), batches.render_shaders.len());
×
2765
            let render_shader_source = &batches.render_shaders[draw_batch.group_index as usize];
×
2766
            trace!("Emit for group index #{}", draw_batch.group_index);
×
2767

2768
            let alpha_mode = batches.alpha_mode;
×
2769

2770
            #[cfg(feature = "trace")]
2771
            let _span_specialize = bevy::utils::tracing::info_span!("specialize").entered();
×
2772
            let render_pipeline_id = specialized_render_pipelines.specialize(
×
2773
                pipeline_cache,
×
2774
                render_pipeline,
×
2775
                ParticleRenderPipelineKey {
×
2776
                    shader: render_shader_source.clone(),
×
2777
                    particle_layout: batches.particle_layout.clone(),
×
2778
                    texture_layout: batches.texture_layout.clone(),
×
2779
                    local_space_simulation,
×
2780
                    use_alpha_mask,
×
2781
                    alpha_mode,
×
2782
                    flipbook,
×
2783
                    needs_uv,
×
2784
                    ribbons,
×
2785
                    #[cfg(all(feature = "2d", feature = "3d"))]
×
2786
                    pipeline_mode,
×
2787
                    msaa_samples,
×
2788
                    hdr: view.hdr,
×
2789
                },
2790
            );
2791
            #[cfg(feature = "trace")]
2792
            _span_specialize.exit();
×
2793

2794
            trace!(
×
2795
                "+ Render pipeline specialized: id={:?} -> group_index={}",
×
2796
                render_pipeline_id,
×
2797
                draw_batch.group_index
×
2798
            );
2799
            trace!(
×
2800
                "+ Add Transparent for batch on draw_entity {:?}: buffer_index={} \
×
2801
                group_index={} spawner_base={} handle={:?}",
×
2802
                draw_entity,
×
2803
                batches.buffer_index,
×
2804
                draw_batch.group_index,
×
2805
                batches.spawner_base,
×
2806
                batches.handle
×
2807
            );
2808
            render_phase.add(
×
2809
                make_bin_key(render_pipeline_id, draw_batch, draw_batch.group_index, view),
×
2810
                draw_entity,
×
2811
                BinnedRenderPhaseType::NonMesh,
×
2812
            );
2813
        }
2814
    }
2815
}
2816

2817
#[allow(clippy::too_many_arguments)]
2818
pub(crate) fn queue_effects(
10✔
2819
    views: Query<(Entity, &VisibleEntities, &ExtractedView)>,
2820
    effects_meta: Res<EffectsMeta>,
2821
    mut render_pipeline: ResMut<ParticlesRenderPipeline>,
2822
    mut specialized_render_pipelines: ResMut<SpecializedRenderPipelines<ParticlesRenderPipeline>>,
2823
    pipeline_cache: Res<PipelineCache>,
2824
    mut effect_bind_groups: ResMut<EffectBindGroups>,
2825
    effect_batches: Query<(Entity, &mut EffectBatches)>,
2826
    effect_draw_batches: Query<(Entity, &mut EffectDrawBatch)>,
2827
    events: Res<EffectAssetEvents>,
2828
    read_params: QueueEffectsReadOnlyParams,
2829
    msaa: Res<Msaa>,
2830
    mut view_entities: Local<FixedBitSet>,
2831
    #[cfg(feature = "2d")] mut transparent_2d_render_phases: ResMut<
2832
        ViewSortedRenderPhases<Transparent2d>,
2833
    >,
2834
    #[cfg(feature = "3d")] mut transparent_3d_render_phases: ResMut<
2835
        ViewSortedRenderPhases<Transparent3d>,
2836
    >,
2837
    #[cfg(feature = "3d")] mut alpha_mask_3d_render_phases: ResMut<
2838
        ViewBinnedRenderPhases<AlphaMask3d>,
2839
    >,
2840
) {
2841
    #[cfg(feature = "trace")]
2842
    let _span = bevy::utils::tracing::info_span!("hanabi:queue_effects").entered();
20✔
2843

2844
    trace!("queue_effects");
×
2845

2846
    // If an image has changed, the GpuImage has (probably) changed
2847
    for event in &events.images {
18✔
2848
        match event {
8✔
2849
            AssetEvent::Added { .. } => None,
5✔
2850
            AssetEvent::LoadedWithDependencies { .. } => None,
×
2851
            AssetEvent::Unused { .. } => None,
×
2852
            AssetEvent::Modified { id } => {
×
2853
                trace!("Destroy bind group of modified image asset {:?}", id);
×
2854
                effect_bind_groups.images.remove(id)
×
2855
            }
2856
            AssetEvent::Removed { id } => {
3✔
2857
                trace!("Destroy bind group of removed image asset {:?}", id);
3✔
2858
                effect_bind_groups.images.remove(id)
3✔
2859
            }
2860
        };
2861
    }
2862

2863
    if effects_meta.spawner_buffer.buffer().is_none() || effects_meta.spawner_buffer.is_empty() {
10✔
2864
        // No spawners are active
2865
        return;
10✔
2866
    }
2867

2868
    // Loop over all 2D cameras/views that need to render effects
2869
    #[cfg(feature = "2d")]
2870
    {
2871
        #[cfg(feature = "trace")]
2872
        let _span_draw = bevy::utils::tracing::info_span!("draw_2d").entered();
×
2873

2874
        let draw_effects_function_2d = read_params
2875
            .draw_functions_2d
2876
            .read()
2877
            .get_id::<DrawEffects>()
2878
            .unwrap();
2879

2880
        // Effects with full alpha blending
2881
        if !views.is_empty() {
2882
            trace!("Emit effect draw calls for alpha blended 2D views...");
×
2883
            emit_sorted_draw(
2884
                &views,
×
2885
                &mut transparent_2d_render_phases,
×
2886
                &mut view_entities,
×
2887
                &effect_batches,
×
2888
                &effect_draw_batches,
×
2889
                &mut render_pipeline,
×
2890
                specialized_render_pipelines.reborrow(),
×
2891
                &pipeline_cache,
×
2892
                msaa.samples(),
×
2893
                |id, entity, draw_batch, _group, _view| Transparent2d {
×
2894
                    draw_function: draw_effects_function_2d,
×
2895
                    pipeline: id,
×
2896
                    entity,
×
2897
                    sort_key: draw_batch.z_sort_key_2d,
×
2898
                    batch_range: 0..1,
×
2899
                    extra_index: PhaseItemExtraIndex::NONE,
×
2900
                },
2901
                #[cfg(feature = "3d")]
2902
                PipelineMode::Camera2d,
2903
            );
2904
        }
2905
    }
2906

2907
    // Loop over all 3D cameras/views that need to render effects
2908
    #[cfg(feature = "3d")]
2909
    {
2910
        #[cfg(feature = "trace")]
2911
        let _span_draw = bevy::utils::tracing::info_span!("draw_3d").entered();
×
2912

2913
        // Effects with full alpha blending
2914
        if !views.is_empty() {
2915
            trace!("Emit effect draw calls for alpha blended 3D views...");
×
2916

2917
            let draw_effects_function_3d = read_params
×
2918
                .draw_functions_3d
×
2919
                .read()
2920
                .get_id::<DrawEffects>()
2921
                .unwrap();
2922

2923
            emit_sorted_draw(
2924
                &views,
×
2925
                &mut transparent_3d_render_phases,
×
2926
                &mut view_entities,
×
2927
                &effect_batches,
×
2928
                &effect_draw_batches,
×
2929
                &mut render_pipeline,
×
2930
                specialized_render_pipelines.reborrow(),
×
2931
                &pipeline_cache,
×
2932
                msaa.samples(),
×
2933
                |id, entity, batch, _group, view| Transparent3d {
×
2934
                    draw_function: draw_effects_function_3d,
×
2935
                    pipeline: id,
×
2936
                    entity,
×
2937
                    distance: view
×
2938
                        .rangefinder3d()
×
2939
                        .distance_translation(&batch.translation_3d),
×
2940
                    batch_range: 0..1,
×
2941
                    extra_index: PhaseItemExtraIndex::NONE,
×
2942
                },
2943
                #[cfg(feature = "2d")]
2944
                PipelineMode::Camera3d,
2945
            );
2946
        }
2947

2948
        // Effects with alpha mask
2949
        if !views.is_empty() {
×
2950
            #[cfg(feature = "trace")]
2951
            let _span_draw = bevy::utils::tracing::info_span!("draw_alphamask").entered();
×
2952

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

2955
            let draw_effects_function_alpha_mask = read_params
×
2956
                .draw_functions_alpha_mask
×
2957
                .read()
2958
                .get_id::<DrawEffects>()
2959
                .unwrap();
2960

2961
            emit_binned_draw(
2962
                &views,
×
2963
                &mut alpha_mask_3d_render_phases,
×
2964
                &mut view_entities,
×
2965
                &effect_batches,
×
2966
                &effect_draw_batches,
×
2967
                &mut render_pipeline,
×
2968
                specialized_render_pipelines.reborrow(),
×
2969
                &pipeline_cache,
×
2970
                msaa.samples(),
×
2971
                |id, _batch, _group, _view| OpaqueNoLightmap3dBinKey {
×
2972
                    pipeline: id,
×
2973
                    draw_function: draw_effects_function_alpha_mask,
×
2974
                    asset_id: AssetId::<Image>::default().untyped(),
×
2975
                    material_bind_group_id: None,
×
2976
                    // },
2977
                    // distance: view
2978
                    //     .rangefinder3d()
2979
                    //     .distance_translation(&batch.translation_3d),
2980
                    // batch_range: 0..1,
2981
                    // extra_index: PhaseItemExtraIndex::NONE,
2982
                },
2983
                #[cfg(feature = "2d")]
2984
                PipelineMode::Camera3d,
2985
                true,
2986
            );
2987
        }
2988
    }
2989
}
2990

2991
/// Prepare GPU resources for effect rendering.
2992
///
2993
/// This system runs in the [`RenderSet::Prepare`] render set, after Bevy has
2994
/// updated the [`ViewUniforms`], which need to be referenced to get access to
2995
/// the current camera view.
2996
pub(crate) fn prepare_gpu_resources(
10✔
2997
    mut effects_meta: ResMut<EffectsMeta>,
2998
    render_device: Res<RenderDevice>,
2999
    view_uniforms: Res<ViewUniforms>,
3000
    render_pipeline: Res<ParticlesRenderPipeline>,
3001
) {
3002
    // Get the binding for the ViewUniform, the uniform data structure containing
3003
    // the Camera data for the current view. If not available, we cannot render
3004
    // anything.
3005
    let Some(view_binding) = view_uniforms.uniforms.binding() else {
20✔
3006
        return;
×
3007
    };
3008

3009
    // Create the bind group for the camera/view parameters
3010
    effects_meta.view_bind_group = Some(render_device.create_bind_group(
3011
        "hanabi:bind_group_camera_view",
3012
        &render_pipeline.view_layout,
3013
        &[
3014
            BindGroupEntry {
3015
                binding: 0,
3016
                resource: view_binding,
3017
            },
3018
            BindGroupEntry {
3019
                binding: 1,
3020
                resource: effects_meta.sim_params_uniforms.binding().unwrap(),
3021
            },
3022
        ],
3023
    ));
3024
}
3025

3026
pub(crate) fn prepare_bind_groups(
10✔
3027
    mut effects_meta: ResMut<EffectsMeta>,
3028
    mut effect_cache: ResMut<EffectCache>,
3029
    mut effect_bind_groups: ResMut<EffectBindGroups>,
3030
    effect_batches: Query<(Entity, &mut EffectBatches)>,
3031
    render_device: Res<RenderDevice>,
3032
    dispatch_indirect_pipeline: Res<DispatchIndirectPipeline>,
3033
    init_pipeline: Res<ParticlesInitPipeline>,
3034
    update_pipeline: Res<ParticlesUpdatePipeline>,
3035
    render_pipeline: ResMut<ParticlesRenderPipeline>,
3036
    gpu_images: Res<RenderAssets<GpuImage>>,
3037
) {
3038
    if effects_meta.spawner_buffer.is_empty() || effects_meta.spawner_buffer.buffer().is_none() {
10✔
3039
        return;
10✔
3040
    }
3041

3042
    {
3043
        #[cfg(feature = "trace")]
3044
        let _span = bevy::utils::tracing::info_span!("shared_bind_groups").entered();
×
3045

3046
        // Create the bind group for the global simulation parameters
3047
        if effects_meta.sim_params_bind_group.is_none() {
×
3048
            effects_meta.sim_params_bind_group = Some(render_device.create_bind_group(
×
3049
                "hanabi:bind_group_sim_params",
×
3050
                &update_pipeline.sim_params_layout, /* FIXME - Shared with vfx_update, is
×
3051
                                                     * that OK? */
×
3052
                &[BindGroupEntry {
×
3053
                    binding: 0,
×
3054
                    resource: effects_meta.sim_params_uniforms.binding().unwrap(),
×
3055
                }],
3056
            ));
3057
        }
3058

3059
        // Create the bind group for the spawner parameters
3060
        // FIXME - This is shared by init and update; should move
3061
        // "update_pipeline.spawner_buffer_layout" out of "update_pipeline"
3062
        trace!(
3063
            "Spawner buffer bind group: size={} aligned_size={}",
×
3064
            GpuSpawnerParams::min_size().get(),
×
3065
            effects_meta.spawner_buffer.aligned_size()
×
3066
        );
3067
        assert!(
×
3068
            effects_meta.spawner_buffer.aligned_size()
×
3069
                >= GpuSpawnerParams::min_size().get() as usize
×
3070
        );
3071
        // Note: we clear effects_meta.spawner_buffer each frame in prepare_effects(),
3072
        // so this bind group is always invalid at the minute and always needs
3073
        // re-creation.
3074
        effects_meta.spawner_bind_group = effects_meta.spawner_buffer.buffer().map(|buffer| {
×
3075
            render_device.create_bind_group(
×
3076
                "hanabi:bind_group_spawner_buffer",
×
3077
                &update_pipeline.spawner_buffer_layout, // FIXME - Shared with init,is that OK?
×
3078
                &[BindGroupEntry {
×
3079
                    binding: 0,
×
3080
                    resource: BindingResource::Buffer(BufferBinding {
×
3081
                        buffer,
×
3082
                        offset: 0,
×
3083
                        size: Some(
×
3084
                            NonZeroU64::new(effects_meta.spawner_buffer.aligned_size() as u64)
×
3085
                                .unwrap(),
×
3086
                        ),
3087
                    }),
3088
                }],
3089
            )
3090
        });
3091

3092
        // Create the bind group for the indirect dispatch of all effects
3093
        effects_meta.dr_indirect_bind_group = match (
×
3094
            effects_meta.render_effect_dispatch_buffer.buffer(),
×
3095
            effects_meta.render_group_dispatch_buffer.buffer(),
×
3096
            effects_meta.dispatch_indirect_buffer.buffer(),
×
3097
            effects_meta.particle_group_buffer.buffer(),
×
3098
            effects_meta.spawner_buffer.buffer(),
×
3099
        ) {
3100
            (
3101
                Some(render_effect_dispatch_buffer),
×
3102
                Some(render_group_dispatch_buffer),
×
3103
                Some(dispatch_indirect_buffer),
×
3104
                Some(particle_group_buffer),
×
3105
                Some(spawner_buffer),
×
3106
            ) => {
×
3107
                Some(render_device.create_bind_group(
×
3108
                    "hanabi:bind_group_vfx_indirect_dr_indirect",
×
3109
                    &dispatch_indirect_pipeline.dispatch_indirect_layout,
×
3110
                    &[
×
3111
                        BindGroupEntry {
×
3112
                            binding: 0,
×
3113
                            resource: BindingResource::Buffer(BufferBinding {
×
3114
                                buffer: render_effect_dispatch_buffer,
×
3115
                                offset: 0,
×
3116
                                size: None, //NonZeroU64::new(256), // Some(GpuRenderIndirect::min_size()),
×
3117
                            }),
3118
                        },
3119
                        BindGroupEntry {
×
3120
                            binding: 1,
×
3121
                            resource: BindingResource::Buffer(BufferBinding {
×
3122
                                buffer: render_group_dispatch_buffer,
×
3123
                                offset: 0,
×
3124
                                size: None, //NonZeroU64::new(256), // Some(GpuRenderIndirect::min_size()),
×
3125
                            }),
3126
                        },
3127
                        BindGroupEntry {
×
3128
                            binding: 2,
×
3129
                            resource: BindingResource::Buffer(BufferBinding {
×
3130
                                buffer: dispatch_indirect_buffer,
×
3131
                                offset: 0,
×
3132
                                size: None, //NonZeroU64::new(256), // Some(GpuDispatchIndirect::min_size()),
×
3133
                            }),
3134
                        },
3135
                        BindGroupEntry {
×
3136
                            binding: 3,
×
3137
                            resource: BindingResource::Buffer(BufferBinding {
×
3138
                                buffer: particle_group_buffer,
×
3139
                                offset: 0,
×
3140
                                size: None,
×
3141
                            }),
3142
                        },
3143
                        BindGroupEntry {
×
3144
                            binding: 4,
×
3145
                            resource: BindingResource::Buffer(BufferBinding {
×
3146
                                buffer: spawner_buffer,
×
3147
                                offset: 0,
×
3148
                                size: None,
×
3149
                            }),
3150
                        },
3151
                    ],
3152
                ))
3153
            }
3154
            _ => None,
×
3155
        };
3156

3157
        let init_render_indirect_bind_group = match (
×
3158
            effects_meta.render_effect_dispatch_buffer.buffer(),
3159
            effects_meta.render_group_dispatch_buffer.buffer(),
3160
        ) {
3161
            (Some(render_effect_dispatch_buffer), Some(render_group_dispatch_buffer)) => {
×
3162
                Some(render_device.create_bind_group(
×
3163
                    "hanabi:bind_group_init_render_dispatch",
×
3164
                    &init_pipeline.render_indirect_layout,
×
3165
                    &[
×
3166
                        BindGroupEntry {
×
3167
                            binding: 0,
×
3168
                            resource: BindingResource::Buffer(BufferBinding {
×
3169
                                buffer: render_effect_dispatch_buffer,
×
3170
                                offset: 0,
×
3171
                                size: Some(effects_meta.gpu_limits.render_effect_indirect_size()),
×
3172
                            }),
3173
                        },
3174
                        BindGroupEntry {
×
3175
                            binding: 1,
×
3176
                            resource: BindingResource::Buffer(BufferBinding {
×
3177
                                buffer: render_group_dispatch_buffer,
×
3178
                                offset: 0,
×
3179
                                size: Some(effects_meta.gpu_limits.render_group_indirect_size()),
×
3180
                            }),
3181
                        },
3182
                        BindGroupEntry {
×
3183
                            binding: 2,
×
3184
                            resource: BindingResource::Buffer(BufferBinding {
×
3185
                                buffer: render_group_dispatch_buffer,
×
3186
                                offset: 0,
×
3187
                                size: Some(effects_meta.gpu_limits.render_group_indirect_size()),
×
3188
                            }),
3189
                        },
3190
                    ],
3191
                ))
3192
            }
3193

3194
            (_, _) => None,
×
3195
        };
3196

3197
        // Create the bind group for the indirect render buffer use in the init shader
3198
        effects_meta.init_render_indirect_bind_group = init_render_indirect_bind_group;
3199
    }
3200

3201
    // Make a copy of the buffer ID before borrowing effects_meta mutably in the
3202
    // loop below
3203
    let Some(indirect_buffer) = effects_meta.dispatch_indirect_buffer.buffer().cloned() else {
×
3204
        return;
×
3205
    };
3206
    let Some(spawner_buffer) = effects_meta.spawner_buffer.buffer().cloned() else {
×
3207
        return;
×
3208
    };
3209

3210
    // Create the per-buffer bind groups
NEW
3211
    trace!("Create per-buffer bind groups...");
×
UNCOV
3212
    for (buffer_index, buffer) in effect_cache.buffers().iter().enumerate() {
×
3213
        #[cfg(feature = "trace")]
3214
        let _span_buffer = bevy::utils::tracing::info_span!("create_buffer_bind_groups").entered();
×
3215

3216
        let Some(buffer) = buffer else {
×
3217
            trace!(
×
3218
                "Effect buffer index #{} has no allocated EffectBuffer, skipped.",
×
3219
                buffer_index
3220
            );
3221
            continue;
×
3222
        };
3223

3224
        // Ensure all effects in this batch have a bind group for the entire buffer of
3225
        // the group, since the update phase runs on an entire group/buffer at
3226
        // once, with all the effect instances in it batched together.
3227
        trace!("effect particle buffer_index=#{}", buffer_index);
×
3228
        effect_bind_groups
×
3229
            .particle_buffers
×
3230
            .entry(buffer_index as u32)
×
3231
            .or_insert_with(|| {
×
3232
                trace!(
×
3233
                    "Create new particle bind groups for buffer_index={} | particle_layout {:?} | property_layout {:?}",
×
3234
                    buffer_index,
×
3235
                    buffer.particle_layout(),
×
3236
                    buffer.property_layout(),
×
3237
                );
3238

3239
                let dispatch_indirect_size = GpuDispatchIndirect::aligned_size(render_device
×
3240
                    .limits()
×
3241
                    .min_storage_buffer_offset_alignment);
×
3242
                let mut entries = vec![
×
3243
                    BindGroupEntry {
×
3244
                        binding: 0,
×
3245
                        resource: buffer.max_binding(),
×
3246
                    },
3247
                    BindGroupEntry {
×
3248
                        binding: 1,
×
3249
                        resource: buffer.indirect_max_binding(),
×
3250
                    },
3251
                    BindGroupEntry {
×
3252
                        binding: 2,
×
3253
                        resource: BindingResource::Buffer(BufferBinding {
×
3254
                            buffer: &indirect_buffer,
×
3255
                            offset: 0,
×
3256
                            size: Some(dispatch_indirect_size),
×
3257
                        }),
3258
                    },
3259
                ];
3260
                if buffer.layout_flags().contains(LayoutFlags::LOCAL_SPACE_SIMULATION) {
×
3261
                    entries.push(BindGroupEntry {
×
3262
                        binding: 3,
×
3263
                        resource: BindingResource::Buffer(BufferBinding {
×
3264
                            buffer: &spawner_buffer,
×
3265
                            offset: 0,
×
3266
                            size: Some(GpuSpawnerParams::min_size()),
×
3267
                        }),
3268
                    });
3269
                }
3270
                trace!("Creating render bind group with {} entries (layout flags: {:?})", entries.len(), buffer.layout_flags());
×
3271
                let render = render_device.create_bind_group(
×
NEW
3272
                    &format!("hanabi:bind_group:render_vfx{buffer_index}_particles")[..],
×
3273
                     buffer.particle_layout_bind_group_with_dispatch(),
×
3274
                     &entries,
×
3275
                );
3276

3277
                BufferBindGroups {
×
3278
                    render,
×
3279
                }
3280
            });
3281
    }
3282

3283
    // Create the per-effect bind groups.
3284
    for (entity, effect_batches) in effect_batches.iter() {
×
3285
        #[cfg(feature = "trace")]
3286
        let _span_buffer = bevy::utils::tracing::info_span!("create_batch_bind_groups").entered();
×
3287

3288
        let effect_cache_id = effect_batches.effect_cache_id;
3289

3290
        // Convert indirect buffer offsets from indices to bytes.
3291
        let first_effect_particle_group_buffer_offset = effects_meta
3292
            .gpu_limits
3293
            .particle_group_offset(effect_batches.first_particle_group_buffer_index)
3294
            as u64;
3295
        let effect_particle_groups_buffer_size = NonZeroU64::try_from(
3296
            u32::from(effects_meta.gpu_limits.particle_group_aligned_size) as u64
3297
                * effect_batches.group_batches.len() as u64,
3298
        )
3299
        .unwrap();
3300
        let group_binding = BufferBinding {
3301
            buffer: effects_meta.particle_group_buffer.buffer().unwrap(),
3302
            offset: first_effect_particle_group_buffer_offset,
3303
            size: Some(effect_particle_groups_buffer_size),
3304
        };
3305

3306
        let Some(Some(effect_buffer)) = effect_cache
×
3307
            .buffers_mut()
3308
            .get_mut(effect_batches.buffer_index as usize)
3309
        else {
3310
            error!("No particle buffer allocated for entity {:?}", entity);
×
3311
            continue;
×
3312
        };
3313

3314
        // Bind group for the init compute shader to simulate particles.
3315
        // TODO - move this creation in RenderSet::PrepareBindGroups
3316
        effect_buffer.create_sim_bind_group(
×
3317
            effect_batches.buffer_index,
×
3318
            &render_device,
×
3319
            group_binding,
×
3320
        );
3321

3322
        if effect_bind_groups
×
3323
            .update_render_indirect_bind_groups
×
3324
            .get(&effect_cache_id)
×
3325
            .is_none()
3326
        {
3327
            let DispatchBufferIndices {
×
3328
                render_effect_metadata_buffer_index: render_effect_dispatch_buffer_index,
×
3329
                first_render_group_dispatch_buffer_index,
×
3330
                ..
×
3331
            } = effect_batches.dispatch_buffer_indices;
×
3332

3333
            let storage_alignment = effects_meta.gpu_limits.storage_buffer_align.get();
×
3334
            let render_effect_indirect_size =
×
3335
                GpuRenderEffectMetadata::aligned_size(storage_alignment);
×
3336
            let total_render_group_indirect_size = NonZeroU64::new(
3337
                GpuRenderGroupIndirect::aligned_size(storage_alignment).get()
×
3338
                    * effect_batches.group_batches.len() as u64,
×
3339
            )
3340
            .unwrap();
3341
            let particles_buffer_layout_update_render_indirect = render_device.create_bind_group(
×
3342
                "hanabi:bind_group_update_render_group_dispatch",
3343
                &update_pipeline.render_indirect_layout,
×
3344
                &[
×
3345
                    BindGroupEntry {
×
3346
                        binding: 0,
×
3347
                        resource: BindingResource::Buffer(BufferBinding {
×
3348
                            buffer: effects_meta.render_effect_dispatch_buffer.buffer().unwrap(),
×
3349
                            offset: effects_meta.gpu_limits.render_effect_indirect_offset(
×
3350
                                render_effect_dispatch_buffer_index.0,
×
3351
                            ),
3352
                            size: Some(render_effect_indirect_size),
×
3353
                        }),
3354
                    },
3355
                    BindGroupEntry {
×
3356
                        binding: 1,
×
3357
                        resource: BindingResource::Buffer(BufferBinding {
×
3358
                            buffer: effects_meta.render_group_dispatch_buffer.buffer().unwrap(),
×
3359
                            offset: effects_meta.gpu_limits.render_group_indirect_offset(
×
3360
                                first_render_group_dispatch_buffer_index.0,
×
3361
                            ),
3362
                            size: Some(total_render_group_indirect_size),
×
3363
                        }),
3364
                    },
3365
                ],
3366
            );
3367

3368
            effect_bind_groups
×
3369
                .update_render_indirect_bind_groups
×
3370
                .insert(
3371
                    effect_cache_id,
×
3372
                    particles_buffer_layout_update_render_indirect,
×
3373
                );
3374
        }
3375

3376
        // Ensure the particle texture(s) are available as GPU resources and that a bind
3377
        // group for them exists FIXME fix this insert+get below
3378
        if !effect_batches.texture_layout.layout.is_empty() {
×
3379
            // This should always be available, as this is cached into the render pipeline
3380
            // just before we start specializing it.
3381
            let Some(material_bind_group_layout) =
×
3382
                render_pipeline.get_material(&effect_batches.texture_layout)
×
3383
            else {
3384
                error!(
×
3385
                    "Failed to find material bind group layout for buffer #{}",
×
3386
                    effect_batches.buffer_index
3387
                );
3388
                continue;
×
3389
            };
3390

3391
            // TODO = move
3392
            let material = Material {
3393
                layout: effect_batches.texture_layout.clone(),
3394
                textures: effect_batches.textures.iter().map(|h| h.id()).collect(),
×
3395
            };
3396
            assert_eq!(material.layout.layout.len(), material.textures.len());
3397

3398
            let bind_group_entries = material.make_entries(&gpu_images);
×
3399

3400
            effect_bind_groups
×
3401
                .material_bind_groups
×
3402
                .entry(material.clone())
×
3403
                .or_insert_with(|| {
×
3404
                    render_device.create_bind_group(
×
3405
                        &format!(
×
3406
                            "hanabi:material_bind_group_{}",
×
3407
                            material.layout.layout.len()
×
3408
                        )[..],
×
3409
                        material_bind_group_layout,
×
3410
                        &bind_group_entries[..],
×
3411
                    )
3412
                });
3413
        }
3414
    }
3415
}
3416

3417
type DrawEffectsSystemState = SystemState<(
3418
    SRes<EffectsMeta>,
3419
    SRes<EffectBindGroups>,
3420
    SRes<PipelineCache>,
3421
    SQuery<Read<ViewUniformOffset>>,
3422
    SQuery<Read<EffectBatches>>,
3423
    SQuery<Read<EffectDrawBatch>>,
3424
)>;
3425

3426
/// Draw function for rendering all active effects for the current frame.
3427
///
3428
/// Effects are rendered in the [`Transparent2d`] phase of the main 2D pass,
3429
/// and the [`Transparent3d`] phase of the main 3D pass.
3430
pub(crate) struct DrawEffects {
3431
    params: DrawEffectsSystemState,
3432
}
3433

3434
impl DrawEffects {
3435
    pub fn new(world: &mut World) -> Self {
3✔
3436
        Self {
3437
            params: SystemState::new(world),
3✔
3438
        }
3439
    }
3440
}
3441

3442
/// Draw all particles of a single effect in view, in 2D or 3D.
3443
///
3444
/// FIXME: use pipeline ID to look up which group index it is.
3445
fn draw<'w>(
×
3446
    world: &'w World,
3447
    pass: &mut TrackedRenderPass<'w>,
3448
    view: Entity,
3449
    entity: Entity,
3450
    pipeline_id: CachedRenderPipelineId,
3451
    params: &mut DrawEffectsSystemState,
3452
) {
3453
    let (effects_meta, effect_bind_groups, pipeline_cache, views, effects, effect_draw_batches) =
×
3454
        params.get(world);
×
3455
    let view_uniform = views.get(view).unwrap();
×
3456
    let effects_meta = effects_meta.into_inner();
×
3457
    let effect_bind_groups = effect_bind_groups.into_inner();
×
3458
    let effect_draw_batch = effect_draw_batches.get(entity).unwrap();
×
3459
    let effect_batches = effects.get(effect_draw_batch.batches_entity).unwrap();
×
3460

3461
    let gpu_limits = &effects_meta.gpu_limits;
×
3462

3463
    let Some(pipeline) = pipeline_cache.into_inner().get_render_pipeline(pipeline_id) else {
×
3464
        return;
×
3465
    };
3466

3467
    trace!("render pass");
×
3468

3469
    pass.set_render_pipeline(pipeline);
×
3470

3471
    // Vertex buffer containing the particle model to draw. Generally a quad.
3472
    pass.set_vertex_buffer(0, effects_meta.vertices.buffer().unwrap().slice(..));
×
3473

3474
    // View properties (camera matrix, etc.)
3475
    pass.set_bind_group(
×
3476
        0,
3477
        effects_meta.view_bind_group.as_ref().unwrap(),
×
3478
        &[view_uniform.offset],
×
3479
    );
3480

3481
    // Particles buffer
3482
    let dispatch_indirect_offset = gpu_limits.dispatch_indirect_offset(effect_batches.buffer_index);
×
3483
    trace!(
×
3484
        "set_bind_group(1): dispatch_indirect_offset={}",
×
3485
        dispatch_indirect_offset
×
3486
    );
3487
    let spawner_base = effect_batches.spawner_base;
×
3488
    let spawner_buffer_aligned = effects_meta.spawner_buffer.aligned_size();
×
3489
    assert!(spawner_buffer_aligned >= GpuSpawnerParams::min_size().get() as usize);
×
3490
    let spawner_offset = spawner_base * spawner_buffer_aligned as u32;
×
3491
    let dyn_uniform_indices: [u32; 2] = [dispatch_indirect_offset, spawner_offset];
×
3492
    let dyn_uniform_indices = if effect_batches
×
3493
        .layout_flags
×
3494
        .contains(LayoutFlags::LOCAL_SPACE_SIMULATION)
×
3495
    {
3496
        &dyn_uniform_indices
×
3497
    } else {
3498
        &dyn_uniform_indices[..1]
×
3499
    };
3500
    pass.set_bind_group(
×
3501
        1,
3502
        effect_bind_groups
×
3503
            .particle_render(effect_batches.buffer_index)
×
3504
            .unwrap(),
×
3505
        dyn_uniform_indices,
×
3506
    );
3507

3508
    // Particle texture
3509
    // TODO = move
3510
    let material = Material {
3511
        layout: effect_batches.texture_layout.clone(),
×
3512
        textures: effect_batches.textures.iter().map(|h| h.id()).collect(),
×
3513
    };
3514
    if !effect_batches.texture_layout.layout.is_empty() {
×
3515
        if let Some(bind_group) = effect_bind_groups.material_bind_groups.get(&material) {
×
3516
            pass.set_bind_group(2, bind_group, &[]);
×
3517
        } else {
3518
            // Texture(s) not ready; skip this drawing for now
3519
            trace!(
×
3520
                "Particle material bind group not available for batch buf={}. Skipping draw call.",
×
3521
                effect_batches.buffer_index,
×
3522
            );
3523
            return; // continue;
×
3524
        }
3525
    }
3526

3527
    let render_indirect_buffer = effects_meta.render_group_dispatch_buffer.buffer().unwrap();
×
3528
    let group_index = effect_draw_batch.group_index;
×
3529
    let effect_batch = &effect_batches.group_batches[group_index as usize];
×
3530

3531
    let render_group_dispatch_indirect_index = effect_batches
×
3532
        .dispatch_buffer_indices
×
3533
        .first_render_group_dispatch_buffer_index
×
3534
        .0
×
3535
        + group_index;
×
3536

3537
    trace!(
×
3538
        "Draw up to {} particles with {} vertices per particle for batch from buffer #{} \
×
3539
            (render_group_dispatch_indirect_index={:?}, group_index={}).",
×
3540
        effect_batch.slice.len(),
×
3541
        effects_meta.vertices.len(),
×
3542
        effect_batches.buffer_index,
×
3543
        render_group_dispatch_indirect_index,
×
3544
        group_index,
×
3545
    );
3546

3547
    pass.draw_indirect(
×
3548
        render_indirect_buffer,
×
3549
        render_group_dispatch_indirect_index as u64
×
3550
            * u32::from(gpu_limits.render_group_indirect_aligned_size) as u64,
×
3551
    );
3552
}
3553

3554
#[cfg(feature = "2d")]
3555
impl Draw<Transparent2d> for DrawEffects {
3556
    fn draw<'w>(
×
3557
        &mut self,
3558
        world: &'w World,
3559
        pass: &mut TrackedRenderPass<'w>,
3560
        view: Entity,
3561
        item: &Transparent2d,
3562
    ) {
3563
        trace!("Draw<Transparent2d>: view={:?}", view);
×
3564
        draw(
3565
            world,
×
3566
            pass,
×
3567
            view,
×
3568
            item.entity,
×
3569
            item.pipeline,
×
3570
            &mut self.params,
×
3571
        );
3572
    }
3573
}
3574

3575
#[cfg(feature = "3d")]
3576
impl Draw<Transparent3d> for DrawEffects {
3577
    fn draw<'w>(
×
3578
        &mut self,
3579
        world: &'w World,
3580
        pass: &mut TrackedRenderPass<'w>,
3581
        view: Entity,
3582
        item: &Transparent3d,
3583
    ) {
3584
        trace!("Draw<Transparent3d>: view={:?}", view);
×
3585
        draw(
3586
            world,
×
3587
            pass,
×
3588
            view,
×
3589
            item.entity,
×
3590
            item.pipeline,
×
3591
            &mut self.params,
×
3592
        );
3593
    }
3594
}
3595

3596
#[cfg(feature = "3d")]
3597
impl Draw<AlphaMask3d> for DrawEffects {
3598
    fn draw<'w>(
×
3599
        &mut self,
3600
        world: &'w World,
3601
        pass: &mut TrackedRenderPass<'w>,
3602
        view: Entity,
3603
        item: &AlphaMask3d,
3604
    ) {
3605
        trace!("Draw<AlphaMask3d>: view={:?}", view);
×
3606
        draw(
3607
            world,
×
3608
            pass,
×
3609
            view,
×
3610
            item.representative_entity,
×
3611
            item.key.pipeline,
×
3612
            &mut self.params,
×
3613
        );
3614
    }
3615
}
3616

3617
fn create_init_particles_bind_group_layout(
×
3618
    render_device: &RenderDevice,
3619
    label: &str,
3620
    particle_layout_min_binding_size: NonZero<u64>,
3621
    property_layout_min_binding_size: Option<NonZero<u64>>,
3622
) -> BindGroupLayout {
3623
    let mut entries = Vec::with_capacity(3);
×
3624
    // (1,0) ParticleBuffer
3625
    entries.push(BindGroupLayoutEntry {
×
3626
        binding: 0,
×
3627
        visibility: ShaderStages::COMPUTE,
×
3628
        ty: BindingType::Buffer {
×
3629
            ty: BufferBindingType::Storage { read_only: false },
×
3630
            has_dynamic_offset: false,
×
3631
            min_binding_size: Some(particle_layout_min_binding_size),
×
3632
        },
3633
        count: None,
×
3634
    });
3635
    // (1,1) IndirectBuffer
3636
    entries.push(BindGroupLayoutEntry {
×
3637
        binding: 1,
×
3638
        visibility: ShaderStages::COMPUTE,
×
3639
        ty: BindingType::Buffer {
×
3640
            ty: BufferBindingType::Storage { read_only: false },
×
3641
            has_dynamic_offset: false,
×
3642
            min_binding_size: BufferSize::new(12),
×
3643
        },
3644
        count: None,
×
3645
    });
3646
    // (1,2) array<ParticleGroup>
3647
    let particle_group_size =
×
3648
        GpuParticleGroup::aligned_size(render_device.limits().min_storage_buffer_offset_alignment);
×
3649
    entries.push(BindGroupLayoutEntry {
×
3650
        binding: 2,
×
3651
        visibility: ShaderStages::COMPUTE,
×
3652
        ty: BindingType::Buffer {
×
3653
            ty: BufferBindingType::Storage { read_only: true },
×
3654
            has_dynamic_offset: false,
×
3655
            min_binding_size: Some(particle_group_size),
×
3656
        },
3657
        count: None,
×
3658
    });
3659
    if let Some(min_binding_size) = property_layout_min_binding_size {
×
3660
        // (1,3) Properties
3661
        entries.push(BindGroupLayoutEntry {
3662
            binding: 3,
3663
            visibility: ShaderStages::COMPUTE,
3664
            ty: BindingType::Buffer {
3665
                ty: BufferBindingType::Storage { read_only: true },
3666
                has_dynamic_offset: false, // TODO
3667
                min_binding_size: Some(min_binding_size),
3668
            },
3669
            count: None,
3670
        });
3671
    }
3672

3673
    trace!(
×
3674
        "Creating particle bind group layout '{}' for init pass with {} entries.",
×
3675
        label,
×
3676
        entries.len()
×
3677
    );
3678
    render_device.create_bind_group_layout(label, &entries)
×
3679
}
3680

3681
fn create_init_render_indirect_bind_group_layout(
1✔
3682
    render_device: &RenderDevice,
3683
    label: &str,
3684
    clone: bool,
3685
) -> BindGroupLayout {
3686
    let storage_alignment = render_device.limits().min_storage_buffer_offset_alignment;
1✔
3687
    let render_effect_indirect_size = GpuRenderEffectMetadata::aligned_size(storage_alignment);
1✔
3688
    let render_group_indirect_size = GpuRenderGroupIndirect::aligned_size(storage_alignment);
1✔
3689

3690
    let mut entries = vec![
1✔
3691
        // @binding(0) var<storage, read_write> render_effect_indirect :
3692
        // RenderEffectMetadata
3693
        BindGroupLayoutEntry {
1✔
3694
            binding: 0,
1✔
3695
            visibility: ShaderStages::COMPUTE,
1✔
3696
            ty: BindingType::Buffer {
1✔
3697
                ty: BufferBindingType::Storage { read_only: false },
1✔
3698
                has_dynamic_offset: true,
1✔
3699
                min_binding_size: Some(render_effect_indirect_size),
1✔
3700
            },
3701
            count: None,
1✔
3702
        },
3703
        // @binding(1) var<storage, read_write> dest_render_group_indirect : RenderGroupIndirect
3704
        BindGroupLayoutEntry {
1✔
3705
            binding: 1,
1✔
3706
            visibility: ShaderStages::COMPUTE,
1✔
3707
            ty: BindingType::Buffer {
1✔
3708
                ty: BufferBindingType::Storage { read_only: false },
1✔
3709
                has_dynamic_offset: true,
1✔
3710
                min_binding_size: Some(render_group_indirect_size),
1✔
3711
            },
3712
            count: None,
1✔
3713
        },
3714
    ];
3715

3716
    if clone {
2✔
3717
        // @binding(2) var<storage, read_write> src_render_group_indirect :
3718
        // RenderGroupIndirect
3719
        entries.push(BindGroupLayoutEntry {
1✔
3720
            binding: 2,
1✔
3721
            visibility: ShaderStages::COMPUTE,
1✔
3722
            ty: BindingType::Buffer {
1✔
3723
                ty: BufferBindingType::Storage { read_only: false },
1✔
3724
                has_dynamic_offset: true,
1✔
3725
                min_binding_size: Some(render_group_indirect_size),
1✔
3726
            },
3727
            count: None,
1✔
3728
        });
3729
    }
3730

3731
    render_device.create_bind_group_layout(label, &entries)
1✔
3732
}
3733

3734
fn create_update_bind_group_layout(
×
3735
    render_device: &RenderDevice,
3736
    label: &str,
3737
    particle_layout_min_binding_size: NonZero<u64>,
3738
    property_layout_min_binding_size: Option<NonZero<u64>>,
3739
) -> BindGroupLayout {
3740
    let particle_group_size =
×
3741
        GpuParticleGroup::aligned_size(render_device.limits().min_storage_buffer_offset_alignment);
×
3742
    let mut entries = vec![
×
3743
        // @binding(0) var<storage, read_write> particle_buffer : ParticleBuffer
3744
        BindGroupLayoutEntry {
×
3745
            binding: 0,
×
3746
            visibility: ShaderStages::COMPUTE,
×
3747
            ty: BindingType::Buffer {
×
3748
                ty: BufferBindingType::Storage { read_only: false },
×
3749
                has_dynamic_offset: false,
×
3750
                min_binding_size: Some(particle_layout_min_binding_size),
×
3751
            },
3752
            count: None,
×
3753
        },
3754
        // @binding(1) var<storage, read_write> indirect_buffer : IndirectBuffer
3755
        BindGroupLayoutEntry {
×
3756
            binding: 1,
×
3757
            visibility: ShaderStages::COMPUTE,
×
3758
            ty: BindingType::Buffer {
×
3759
                ty: BufferBindingType::Storage { read_only: false },
×
3760
                has_dynamic_offset: false,
×
3761
                min_binding_size: BufferSize::new(INDIRECT_INDEX_SIZE as _),
×
3762
            },
3763
            count: None,
×
3764
        },
3765
        // @binding(2) var<storage, read> particle_groups : array<ParticleGroup>
3766
        BindGroupLayoutEntry {
×
3767
            binding: 2,
×
3768
            visibility: ShaderStages::COMPUTE,
×
3769
            ty: BindingType::Buffer {
×
3770
                ty: BufferBindingType::Storage { read_only: true },
×
3771
                has_dynamic_offset: false,
×
3772
                min_binding_size: Some(particle_group_size),
×
3773
            },
3774
            count: None,
×
3775
        },
3776
    ];
3777
    if let Some(property_layout_min_binding_size) = property_layout_min_binding_size {
×
3778
        // @binding(3) var<storage, read> properties : Properties
3779
        entries.push(BindGroupLayoutEntry {
3780
            binding: 3,
3781
            visibility: ShaderStages::COMPUTE,
3782
            ty: BindingType::Buffer {
3783
                ty: BufferBindingType::Storage { read_only: true },
3784
                has_dynamic_offset: false, // TODO
3785
                min_binding_size: Some(property_layout_min_binding_size),
3786
            },
3787
            count: None,
3788
        });
3789
    }
3790

3791
    trace!(
×
3792
        "Creating particle bind group layout '{}' for update pass with {} entries.",
×
3793
        label,
×
3794
        entries.len()
×
3795
    );
3796
    render_device.create_bind_group_layout(label, &entries)
×
3797
}
3798

3799
/// Render node to run the simulation sub-graph once per frame.
3800
///
3801
/// This node doesn't simulate anything by itself, but instead schedules the
3802
/// simulation sub-graph, where other nodes like [`VfxSimulateNode`] do the
3803
/// actual simulation.
3804
///
3805
/// The simulation sub-graph is scheduled to run before the [`CameraDriverNode`]
3806
/// renders all the views, such that rendered views have access to the
3807
/// just-simulated particles to render them.
3808
///
3809
/// [`CameraDriverNode`]: bevy::render::camera::CameraDriverNode
3810
pub(crate) struct VfxSimulateDriverNode;
3811

3812
impl Node for VfxSimulateDriverNode {
3813
    fn run(
10✔
3814
        &self,
3815
        graph: &mut RenderGraphContext,
3816
        _render_context: &mut RenderContext,
3817
        _world: &World,
3818
    ) -> Result<(), NodeRunError> {
3819
        graph.run_sub_graph(
10✔
3820
            crate::plugin::simulate_graph::HanabiSimulateGraph,
10✔
3821
            vec![],
10✔
3822
            None,
10✔
3823
        )?;
3824
        Ok(())
10✔
3825
    }
3826
}
3827

3828
/// Render node to run the simulation of all effects once per frame.
3829
///
3830
/// Runs inside the simulation sub-graph, looping over all extracted effect
3831
/// batches to simulate them.
3832
pub(crate) struct VfxSimulateNode {
3833
    /// Query to retrieve the batches of effects to simulate and render.
3834
    effect_query: QueryState<(Entity, Read<EffectBatches>)>,
3835
}
3836

3837
impl VfxSimulateNode {
3838
    /// Output particle buffer for that view. TODO - how to handle multiple
3839
    /// buffers?! Should use Entity instead??
3840
    // pub const OUT_PARTICLE_BUFFER: &'static str = "particle_buffer";
3841

3842
    /// Create a new node for simulating the effects of the given world.
3843
    pub fn new(world: &mut World) -> Self {
1✔
3844
        Self {
3845
            effect_query: QueryState::new(world),
1✔
3846
        }
3847
    }
3848
}
3849

3850
impl Node for VfxSimulateNode {
3851
    fn input(&self) -> Vec<SlotInfo> {
1✔
3852
        vec![]
1✔
3853
    }
3854

3855
    fn update(&mut self, world: &mut World) {
10✔
3856
        trace!("VfxSimulateNode::update()");
10✔
3857
        self.effect_query.update_archetypes(world);
10✔
3858
    }
3859

3860
    fn run(
10✔
3861
        &self,
3862
        _graph: &mut RenderGraphContext,
3863
        render_context: &mut RenderContext,
3864
        world: &World,
3865
    ) -> Result<(), NodeRunError> {
3866
        trace!("VfxSimulateNode::run()");
10✔
3867

3868
        // Get the Entity containing the ViewEffectsEntity component used as container
3869
        // for the input data for this node.
3870
        // let view_entity = graph.get_input_entity(Self::IN_VIEW)?;
3871
        let pipeline_cache = world.resource::<PipelineCache>();
10✔
3872

3873
        let effects_meta = world.resource::<EffectsMeta>();
10✔
3874
        let effect_cache = world.resource::<EffectCache>();
10✔
3875
        let effect_bind_groups = world.resource::<EffectBindGroups>();
10✔
3876
        let dispatch_indirect_pipeline = world.resource::<DispatchIndirectPipeline>();
10✔
3877
        // let render_queue = world.resource::<RenderQueue>();
3878

3879
        // Make sure to schedule any buffer copy from changed effects before accessing
3880
        // them
3881
        {
3882
            let command_encoder = render_context.command_encoder();
10✔
3883
            effects_meta
10✔
3884
                .dispatch_indirect_buffer
10✔
3885
                .write_buffer(command_encoder);
10✔
3886
            effects_meta
10✔
3887
                .render_effect_dispatch_buffer
10✔
3888
                .write_buffer(command_encoder);
10✔
3889
            effects_meta
10✔
3890
                .render_group_dispatch_buffer
10✔
3891
                .write_buffer(command_encoder);
10✔
3892
        }
3893

3894
        // Compute init pass
3895
        // let mut total_group_count = 0;
3896
        {
3897
            {
3898
                trace!("init: loop over effect batches...");
10✔
3899

3900
                // Dispatch init compute jobs
3901
                for (entity, batches) in self.effect_query.iter_manual(world) {
10✔
3902
                    for &dest_group_index in batches.group_order.iter() {
×
3903
                        let initializer = &batches.initializers[dest_group_index as usize];
×
3904
                        let dest_render_group_dispatch_buffer_index = BufferTableId(
3905
                            batches
×
3906
                                .dispatch_buffer_indices
×
3907
                                .first_render_group_dispatch_buffer_index
×
3908
                                .0
×
3909
                                + dest_group_index,
×
3910
                        );
3911

3912
                        // Destination group spawners are packed one after one another.
3913
                        let spawner_base = batches.spawner_base + dest_group_index;
×
3914
                        let spawner_buffer_aligned = effects_meta.spawner_buffer.aligned_size();
×
3915
                        assert!(
×
3916
                            spawner_buffer_aligned >= GpuSpawnerParams::min_size().get() as usize
×
3917
                        );
3918
                        let spawner_offset = spawner_base * spawner_buffer_aligned as u32;
×
3919

3920
                        match initializer {
×
3921
                            EffectInitializer::Spawner(effect_spawner) => {
×
3922
                                let mut compute_pass = render_context
×
3923
                                    .command_encoder()
3924
                                    .begin_compute_pass(&ComputePassDescriptor {
×
3925
                                        label: Some("hanabi:init"),
×
3926
                                        timestamp_writes: None,
×
3927
                                    });
3928

3929
                                let render_effect_dispatch_buffer_index = batches
×
3930
                                    .dispatch_buffer_indices
×
3931
                                    .render_effect_metadata_buffer_index;
×
3932

3933
                                // FIXME - Currently we unconditionally count
3934
                                // all groups because the dispatch pass always
3935
                                // runs on all groups. We should consider if
3936
                                // it's worth skipping e.g. dormant or finished
3937
                                // effects at the cost of extra complexity.
3938
                                // total_group_count += batches.group_batches.len() as u32;
3939

3940
                                let Some(init_pipeline) = pipeline_cache.get_compute_pipeline(
×
3941
                                    batches.init_and_update_pipeline_ids[dest_group_index as usize]
3942
                                        .init,
3943
                                ) else {
3944
                                    if let CachedPipelineState::Err(err) = pipeline_cache
×
3945
                                        .get_compute_pipeline_state(
3946
                                            batches.init_and_update_pipeline_ids
×
3947
                                                [dest_group_index as usize]
×
3948
                                                .init,
×
3949
                                        )
3950
                                    {
3951
                                        error!(
3952
                                            "Failed to find init pipeline #{} for effect {:?}: \
×
3953
                                             {:?}",
×
3954
                                            batches.init_and_update_pipeline_ids
×
3955
                                                [dest_group_index as usize]
×
3956
                                                .init
×
3957
                                                .id(),
×
3958
                                            entity,
3959
                                            err
3960
                                        );
3961
                                    }
3962
                                    continue;
×
3963
                                };
3964

3965
                                // Do not dispatch any init work if there's nothing to spawn this
3966
                                // frame
3967
                                let spawn_count = effect_spawner.spawn_count;
3968
                                if spawn_count == 0 {
3969
                                    continue;
×
3970
                                }
3971

3972
                                const WORKGROUP_SIZE: u32 = 64;
3973
                                let workgroup_count =
3974
                                    (spawn_count + WORKGROUP_SIZE - 1) / WORKGROUP_SIZE;
3975

3976
                                let effect_cache_id = batches.effect_cache_id;
3977

3978
                                // for (effect_entity, effect_slice) in
3979
                                // effects_meta.entity_map.iter()
3980
                                // Retrieve the ExtractedEffect from the entity
3981
                                // trace!("effect_entity={:?} effect_slice={:?}", effect_entity,
3982
                                // effect_slice); let effect =
3983
                                // self.effect_query.get_manual(world, *effect_entity).unwrap();
3984

3985
                                // Get the slice to init
3986
                                // let effect_slice = effects_meta.get(&effect_entity);
3987
                                // let effect_group =
3988
                                //     &effects_meta.effect_cache.buffers()[batch.buffer_index as
3989
                                // usize];
3990
                                let Some(particles_init_bind_group) =
×
3991
                                    effect_cache.init_bind_group(effect_cache_id)
3992
                                else {
3993
                                    error!(
×
3994
                                        "Failed to find init particle buffer bind group for \
×
3995
                                         entity {:?}",
×
3996
                                        entity
3997
                                    );
3998
                                    continue;
×
3999
                                };
4000

4001
                                let render_effect_indirect_offset =
4002
                                    effects_meta.gpu_limits.render_effect_indirect_offset(
4003
                                        render_effect_dispatch_buffer_index.0,
4004
                                    );
4005

4006
                                let render_group_indirect_offset =
4007
                                    effects_meta.gpu_limits.render_group_indirect_offset(
4008
                                        dest_render_group_dispatch_buffer_index.0,
4009
                                    );
4010

4011
                                trace!(
4012
                                    "record commands for init pipeline of effect {:?} \
×
4013
                                        (spawn {} = {} workgroups) spawner_base={} \
×
4014
                                        spawner_offset={} \
×
4015
                                        render_effect_indirect_offset={} \
×
4016
                                        first_render_group_indirect_offset={}...",
×
4017
                                    batches.handle,
4018
                                    spawn_count,
4019
                                    workgroup_count,
4020
                                    spawner_base,
4021
                                    spawner_offset,
4022
                                    render_effect_indirect_offset,
4023
                                    render_group_indirect_offset,
4024
                                );
4025

4026
                                // Setup compute pass
4027
                                compute_pass.set_pipeline(init_pipeline);
×
4028
                                compute_pass.set_bind_group(
×
4029
                                    0,
4030
                                    effects_meta.sim_params_bind_group.as_ref().unwrap(),
×
4031
                                    &[],
×
4032
                                );
4033
                                compute_pass.set_bind_group(1, particles_init_bind_group, &[]);
×
4034
                                compute_pass.set_bind_group(
×
4035
                                    2,
4036
                                    effects_meta.spawner_bind_group.as_ref().unwrap(),
×
4037
                                    &[spawner_offset],
×
4038
                                );
4039
                                compute_pass.set_bind_group(
×
4040
                                    3,
4041
                                    effects_meta
×
4042
                                        .init_render_indirect_bind_group
×
4043
                                        .as_ref()
×
4044
                                        .unwrap(),
×
4045
                                    &[
×
4046
                                        render_effect_indirect_offset as u32,
×
4047
                                        render_group_indirect_offset as u32,
×
4048
                                        render_group_indirect_offset as u32,
×
4049
                                    ],
4050
                                );
4051
                                compute_pass.dispatch_workgroups(workgroup_count, 1, 1);
×
4052
                                trace!("init compute dispatched");
×
4053
                            }
4054

4055
                            EffectInitializer::Cloner(EffectCloner {
4056
                                clone_this_frame: spawn_this_frame,
×
4057
                                ..
×
4058
                            }) => {
×
4059
                                if !spawn_this_frame {
×
4060
                                    continue;
×
4061
                                }
4062

4063
                                let mut compute_pass = render_context
×
4064
                                    .command_encoder()
4065
                                    .begin_compute_pass(&ComputePassDescriptor {
×
4066
                                        label: Some("hanabi:clone"),
×
4067
                                        timestamp_writes: None,
×
4068
                                    });
4069

4070
                                let clone_pipeline_id = batches.init_and_update_pipeline_ids
×
4071
                                    [dest_group_index as usize]
×
4072
                                    .init;
×
4073

4074
                                let effect_cache_id = batches.effect_cache_id;
×
4075

4076
                                let Some(clone_pipeline) =
×
4077
                                    pipeline_cache.get_compute_pipeline(clone_pipeline_id)
4078
                                else {
4079
                                    if let CachedPipelineState::Err(err) =
×
4080
                                        pipeline_cache.get_compute_pipeline_state(clone_pipeline_id)
×
4081
                                    {
4082
                                        error!(
4083
                                            "Failed to find clone pipeline #{} for effect \
×
4084
                                                    {:?}: {:?}",
×
4085
                                            clone_pipeline_id.id(),
×
4086
                                            entity,
4087
                                            err
4088
                                        );
4089
                                    }
4090
                                    continue;
×
4091
                                };
4092

4093
                                let Some(particles_init_bind_group) =
×
4094
                                    effect_cache.init_bind_group(effect_cache_id)
4095
                                else {
4096
                                    error!(
×
4097
                                        "Failed to find clone particle buffer bind group \
×
4098
                                                 for entity {:?}, effect cache ID {:?}",
×
4099
                                        entity, effect_cache_id
4100
                                    );
4101
                                    continue;
×
4102
                                };
4103

4104
                                let render_effect_dispatch_buffer_index = batches
4105
                                    .dispatch_buffer_indices
4106
                                    .render_effect_metadata_buffer_index;
4107
                                let clone_dest_render_group_dispatch_buffer_index = batches
4108
                                    .dispatch_buffer_indices
4109
                                    .trail_dispatch_buffer_indices[&dest_group_index]
4110
                                    .dest;
4111
                                let clone_src_render_group_dispatch_buffer_index = batches
4112
                                    .dispatch_buffer_indices
4113
                                    .trail_dispatch_buffer_indices[&dest_group_index]
4114
                                    .src;
4115

4116
                                let render_effect_indirect_offset =
4117
                                    effects_meta.gpu_limits.render_effect_indirect_offset(
4118
                                        render_effect_dispatch_buffer_index.0,
4119
                                    );
4120

4121
                                let clone_dest_render_group_indirect_offset =
4122
                                    effects_meta.gpu_limits.render_group_indirect_offset(
4123
                                        clone_dest_render_group_dispatch_buffer_index.0,
4124
                                    );
4125
                                let clone_src_render_group_indirect_offset =
4126
                                    effects_meta.gpu_limits.render_group_indirect_offset(
4127
                                        clone_src_render_group_dispatch_buffer_index.0,
4128
                                    );
4129

4130
                                compute_pass.set_pipeline(clone_pipeline);
4131
                                compute_pass.set_bind_group(
4132
                                    0,
4133
                                    effects_meta.sim_params_bind_group.as_ref().unwrap(),
4134
                                    &[],
4135
                                );
4136
                                compute_pass.set_bind_group(1, particles_init_bind_group, &[]);
4137
                                compute_pass.set_bind_group(
4138
                                    2,
4139
                                    effects_meta.spawner_bind_group.as_ref().unwrap(),
4140
                                    &[spawner_offset],
4141
                                );
4142
                                compute_pass.set_bind_group(
4143
                                    3,
4144
                                    effects_meta
4145
                                        .init_render_indirect_bind_group
4146
                                        .as_ref()
4147
                                        .unwrap(),
4148
                                    &[
4149
                                        render_effect_indirect_offset as u32,
4150
                                        clone_dest_render_group_indirect_offset as u32,
4151
                                        clone_src_render_group_indirect_offset as u32,
4152
                                    ],
4153
                                );
4154

4155
                                if let Some(dispatch_indirect_buffer) =
×
4156
                                    effects_meta.dispatch_indirect_buffer.buffer()
4157
                                {
4158
                                    compute_pass.dispatch_workgroups_indirect(
4159
                                        dispatch_indirect_buffer,
4160
                                        clone_src_render_group_indirect_offset,
4161
                                    );
4162
                                }
4163
                                trace!("clone compute dispatched");
×
4164
                            }
4165
                        }
4166
                    }
4167
                }
4168
            }
4169
        }
4170

4171
        // Compute indirect dispatch pass
4172
        if effects_meta.spawner_buffer.buffer().is_some()
10✔
4173
            && !effects_meta.spawner_buffer.is_empty()
×
4174
            && effects_meta.dr_indirect_bind_group.is_some()
×
4175
            && effects_meta.sim_params_bind_group.is_some()
×
4176
        {
4177
            // Only start a compute pass if there's an effect; makes things clearer in
4178
            // debugger.
4179
            let mut compute_pass =
×
4180
                render_context
×
4181
                    .command_encoder()
4182
                    .begin_compute_pass(&ComputePassDescriptor {
×
4183
                        label: Some("hanabi:indirect_dispatch"),
×
4184
                        timestamp_writes: None,
×
4185
                    });
4186

4187
            // Dispatch indirect dispatch compute job
4188
            trace!("record commands for indirect dispatch pipeline...");
×
4189

4190
            // FIXME - The `vfx_indirect` shader assumes a contiguous array of ParticleGroup
4191
            // structures. So we need to pass the full array size, and we
4192
            // just update the unused groups for nothing. Otherwise we might
4193
            // update some unused group and miss some used ones, if there's any gap
4194
            // in the array.
4195
            const WORKGROUP_SIZE: u32 = 64;
4196
            let total_group_count = effects_meta.particle_group_buffer.len() as u32;
×
4197
            let workgroup_count = (total_group_count + WORKGROUP_SIZE - 1) / WORKGROUP_SIZE;
×
4198

4199
            // Setup compute pass
4200
            compute_pass.set_pipeline(&dispatch_indirect_pipeline.pipeline);
×
4201
            compute_pass.set_bind_group(
×
4202
                0,
4203
                // FIXME - got some unwrap() panic here, investigate... possibly race
4204
                // condition!
4205
                effects_meta.dr_indirect_bind_group.as_ref().unwrap(),
×
4206
                &[],
×
4207
            );
4208
            compute_pass.set_bind_group(
×
4209
                1,
4210
                effects_meta.sim_params_bind_group.as_ref().unwrap(),
×
4211
                &[],
×
4212
            );
4213
            compute_pass.dispatch_workgroups(workgroup_count, 1, 1);
×
4214
            trace!(
×
4215
                "indirect dispatch compute dispatched: num_batches={} workgroup_count={}",
×
4216
                total_group_count,
4217
                workgroup_count
4218
            );
4219
        }
4220

4221
        // Compute update pass
4222
        {
4223
            let mut compute_pass =
10✔
4224
                render_context
10✔
4225
                    .command_encoder()
4226
                    .begin_compute_pass(&ComputePassDescriptor {
10✔
4227
                        label: Some("hanabi:update"),
10✔
4228
                        timestamp_writes: None,
10✔
4229
                    });
4230

4231
            // Dispatch update compute jobs
4232
            for (entity, batches) in self.effect_query.iter_manual(world) {
×
4233
                let effect_cache_id = batches.effect_cache_id;
×
4234

4235
                let Some(particles_update_bind_group) =
×
4236
                    effect_cache.update_bind_group(effect_cache_id)
×
4237
                else {
4238
                    error!(
×
4239
                        "Failed to find update particle buffer bind group for entity {:?}, effect cache ID {:?}",
×
4240
                        entity, effect_cache_id
4241
                    );
4242
                    continue;
×
4243
                };
4244

4245
                let first_update_group_dispatch_buffer_index = batches
4246
                    .dispatch_buffer_indices
4247
                    .first_update_group_dispatch_buffer_index;
4248

4249
                let Some(update_render_indirect_bind_group) = &effect_bind_groups
×
4250
                    .update_render_indirect_bind_groups
4251
                    .get(&effect_cache_id)
4252
                else {
4253
                    error!(
×
4254
                        "Failed to find update render indirect bind group for effect cache ID: {:?}, IDs present: {:?}",
×
4255
                        effect_cache_id,
×
4256
                        effect_bind_groups
×
4257
                            .update_render_indirect_bind_groups
×
4258
                            .keys()
×
4259
                            .collect::<Vec<_>>()
×
4260
                    );
4261
                    continue;
×
4262
                };
4263

4264
                for &group_index in batches.group_order.iter() {
×
4265
                    let init_and_update_pipeline_id =
×
4266
                        &batches.init_and_update_pipeline_ids[group_index as usize];
×
4267
                    let Some(update_pipeline) =
×
4268
                        pipeline_cache.get_compute_pipeline(init_and_update_pipeline_id.update)
×
4269
                    else {
4270
                        if let CachedPipelineState::Err(err) = pipeline_cache
×
4271
                            .get_compute_pipeline_state(init_and_update_pipeline_id.update)
×
4272
                        {
4273
                            error!(
4274
                                "Failed to find update pipeline #{} for effect {:?}, group {}: {:?}",
×
4275
                                init_and_update_pipeline_id.update.id(),
×
4276
                                entity,
4277
                                group_index,
4278
                                err
4279
                            );
4280
                        }
4281
                        continue;
×
4282
                    };
4283

4284
                    let update_group_dispatch_buffer_offset =
4285
                        effects_meta.gpu_limits.dispatch_indirect_offset(
4286
                            first_update_group_dispatch_buffer_index.0 + group_index,
4287
                        );
4288

4289
                    // Destination group spawners are packed one after one another.
4290
                    let spawner_base = batches.spawner_base + group_index;
4291
                    let spawner_buffer_aligned = effects_meta.spawner_buffer.aligned_size();
4292
                    assert!(spawner_buffer_aligned >= GpuSpawnerParams::min_size().get() as usize);
4293
                    let spawner_offset = spawner_base * spawner_buffer_aligned as u32;
×
4294

4295
                    // for (effect_entity, effect_slice) in effects_meta.entity_map.iter()
4296
                    // Retrieve the ExtractedEffect from the entity
4297
                    // trace!("effect_entity={:?} effect_slice={:?}", effect_entity,
4298
                    // effect_slice); let effect =
4299
                    // self.effect_query.get_manual(world, *effect_entity).unwrap();
4300

4301
                    // Get the slice to update
4302
                    // let effect_slice = effects_meta.get(&effect_entity);
4303
                    // let effect_group =
4304
                    //     &effects_meta.effect_cache.buffers()[batch.buffer_index as usize];
4305

4306
                    trace!(
×
4307
                        "record commands for update pipeline of effect {:?} \
×
4308
                        spawner_base={} update_group_dispatch_buffer_offset={}…",
×
4309
                        batches.handle,
4310
                        spawner_base,
4311
                        update_group_dispatch_buffer_offset,
4312
                    );
4313

4314
                    // Setup compute pass
4315
                    // compute_pass.set_pipeline(&effect_group.update_pipeline);
4316
                    compute_pass.set_pipeline(update_pipeline);
×
4317
                    compute_pass.set_bind_group(
×
4318
                        0,
4319
                        effects_meta.sim_params_bind_group.as_ref().unwrap(),
×
4320
                        &[],
×
4321
                    );
4322
                    compute_pass.set_bind_group(1, particles_update_bind_group, &[]);
×
4323
                    compute_pass.set_bind_group(
×
4324
                        2,
4325
                        effects_meta.spawner_bind_group.as_ref().unwrap(),
×
4326
                        &[spawner_offset],
×
4327
                    );
4328
                    compute_pass.set_bind_group(3, update_render_indirect_bind_group, &[]);
×
4329

4330
                    if let Some(buffer) = effects_meta.dispatch_indirect_buffer.buffer() {
×
4331
                        trace!(
4332
                            "dispatch_workgroups_indirect: buffer={:?} offset={}",
×
4333
                            buffer,
4334
                            update_group_dispatch_buffer_offset,
4335
                        );
4336
                        compute_pass.dispatch_workgroups_indirect(
×
4337
                            buffer,
×
4338
                            update_group_dispatch_buffer_offset as u64,
×
4339
                        );
4340
                        // TODO - offset
4341
                    }
4342

4343
                    trace!("update compute dispatched");
×
4344
                }
4345
            }
4346
        }
4347

4348
        Ok(())
10✔
4349
    }
4350
}
4351

4352
// FIXME - Remove this, handle it properly with a BufferTable::insert_many() or
4353
// so...
4354
fn allocate_sequential_buffers<T, I>(
×
4355
    buffer_table: &mut BufferTable<T>,
4356
    iterator: I,
4357
) -> BufferTableId
4358
where
4359
    T: Pod + ShaderSize,
4360
    I: Iterator<Item = T>,
4361
{
4362
    let mut first_buffer = None;
×
4363
    for (object_index, object) in iterator.enumerate() {
×
4364
        let buffer = buffer_table.insert(object);
×
4365
        match first_buffer {
×
4366
            None => first_buffer = Some(buffer),
×
4367
            Some(ref first_buffer) => {
×
4368
                if first_buffer.0 + object_index as u32 != buffer.0 {
×
4369
                    error!(
×
4370
                        "Allocator didn't allocate sequential indices (expected {:?}, got {:?}). \
×
4371
                        Expect trouble!",
×
4372
                        first_buffer.0 + object_index as u32,
×
4373
                        buffer.0
×
4374
                    );
4375
                }
4376
            }
4377
        }
4378
    }
4379

4380
    first_buffer.expect("No buffers allocated")
×
4381
}
4382

4383
#[cfg(test)]
4384
mod tests {
4385
    use super::*;
4386

4387
    #[test]
4388
    fn layout_flags() {
4389
        let flags = LayoutFlags::default();
4390
        assert_eq!(flags, LayoutFlags::NONE);
4391
    }
4392

4393
    #[cfg(feature = "gpu_tests")]
4394
    #[test]
4395
    fn gpu_limits() {
4396
        use crate::test_utils::MockRenderer;
4397

4398
        let renderer = MockRenderer::new();
4399
        let device = renderer.device();
4400
        let limits = GpuLimits::from_device(&device);
4401

4402
        // assert!(limits.storage_buffer_align().get() >= 1);
4403
        assert!(
4404
            limits.render_effect_indirect_offset(256)
4405
                >= 256 * GpuRenderEffectMetadata::min_size().get()
4406
        );
4407
        assert!(
4408
            limits.render_group_indirect_offset(256)
4409
                >= 256 * GpuRenderGroupIndirect::min_size().get()
4410
        );
4411
        assert!(
4412
            limits.dispatch_indirect_offset(256) as u64
4413
                >= 256 * GpuDispatchIndirect::min_size().get()
4414
        );
4415
    }
4416
}
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