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

djeedai / bevy_hanabi / 11543837292

27 Oct 2024 09:10PM UTC coverage: 57.849% (-1.2%) from 59.035%
11543837292

Pull #387

github

web-flow
Merge a72c10537 into 75f07d778
Pull Request #387: Unify the clone modifier and spawners, and fix races.

114 of 621 new or added lines in 7 files covered. (18.36%)

23 existing lines in 5 files now uncovered.

3534 of 6109 relevant lines covered (57.85%)

23.02 hits per line

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

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

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

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

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

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

77
use self::batch::EffectBatches;
78

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

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

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

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

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

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

150
impl From<SimParams> for GpuSimParams {
151
    fn from(src: SimParams) -> Self {
10✔
152
        Self {
153
            delta_time: src.delta_time,
10✔
154
            time: src.time as f32,
10✔
155
            virtual_delta_time: src.virtual_delta_time,
10✔
156
            virtual_time: src.virtual_time as f32,
10✔
157
            real_delta_time: src.real_delta_time,
10✔
158
            real_time: src.real_time as f32,
10✔
159
            ..default()
160
        }
161
    }
162
}
163

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

180
impl From<Mat4> for GpuCompressedTransform {
181
    fn from(value: Mat4) -> Self {
×
182
        let tr = value.transpose();
×
183
        #[cfg(test)]
184
        crate::test_utils::assert_approx_eq!(tr.w_axis, Vec4::W);
185
        Self {
186
            x_row: tr.x_axis,
×
187
            y_row: tr.y_axis,
×
188
            z_row: tr.z_axis,
×
189
        }
190
    }
191
}
192

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

206
impl GpuCompressedTransform {
207
    /// Returns the translation as represented by this transform.
208
    #[allow(dead_code)]
209
    pub fn translation(&self) -> Vec3 {
×
210
        Vec3 {
211
            x: self.x_row.w,
×
212
            y: self.y_row.w,
×
213
            z: self.z_row.w,
×
214
        }
215
    }
216
}
217

218
/// Extension trait for shader types stored in a WGSL storage buffer.
219
pub(crate) trait StorageType {
220
    /// Get the aligned size of this type based on the given alignment in bytes.
221
    fn aligned_size(alignment: u32) -> NonZeroU64;
222

223
    /// Get the WGSL padding code to append to the GPU struct to align it.
224
    ///
225
    /// This is useful if the struct needs to be bound directly with a dynamic
226
    /// bind group offset, which requires the offset to be a multiple of a GPU
227
    /// device specific alignment value.
228
    fn padding_code(alignment: u32) -> String;
229
}
230

231
impl<T: ShaderType> StorageType for T {
232
    fn aligned_size(alignment: u32) -> NonZeroU64 {
43✔
233
        NonZeroU64::new(next_multiple_of(T::min_size().get() as usize, alignment as usize) as u64)
43✔
234
            .unwrap()
235
    }
236

237
    fn padding_code(alignment: u32) -> String {
25✔
238
        let aligned_size = T::aligned_size(alignment);
25✔
239
        trace!(
25✔
240
            "Aligning {} to {} bytes as device limits requires. Aligned size: {} bytes.",
×
241
            stringify!(T),
×
242
            alignment,
×
243
            aligned_size
×
244
        );
245

246
        // We need to pad the Spawner WGSL struct based on the device padding so that we
247
        // can use it as an array element but also has a direct struct binding.
248
        if T::min_size() != aligned_size {
25✔
249
            let padding_size = aligned_size.get() - T::min_size().get();
25✔
250
            assert!(padding_size % 4 == 0);
25✔
251
            format!("padding: array<u32, {}>", padding_size / 4)
25✔
252
        } else {
253
            "".to_string()
×
254
        }
255
    }
256
}
257

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

287
// FIXME - min_storage_buffer_offset_alignment
288
#[repr(C)]
289
#[derive(Debug, Clone, Copy, Pod, Zeroable, ShaderType)]
290
pub struct GpuDispatchIndirect {
291
    pub x: u32,
292
    pub y: u32,
293
    pub z: u32,
294
    pub pong: u32,
295
}
296

297
impl Default for GpuDispatchIndirect {
298
    fn default() -> Self {
×
299
        Self {
300
            x: 0,
301
            y: 1,
302
            z: 1,
303
            pong: 0,
304
        }
305
    }
306
}
307

308
#[repr(C)]
309
#[derive(Debug, Default, Clone, Copy, Pod, Zeroable, ShaderType)]
310
pub struct GpuRenderEffectMetadata {
311
    pub ping: u32,
312
}
313

314
#[repr(C)]
315
#[derive(Debug, Default, Clone, Copy, Pod, Zeroable, ShaderType)]
316
pub struct GpuRenderGroupIndirect {
317
    pub vertex_count: u32,
318
    pub instance_count: u32,
319
    pub vertex_offset: i32,
320
    pub base_instance: u32,
321
    //
322
    pub alive_count: u32,
323
    pub max_update: u32,
324
    pub dead_count: u32,
325
    pub max_spawn: u32,
326
}
327

328
/// Stores metadata about each particle group.
329
///
330
/// This is written by the CPU and read by the GPU.
331
#[repr(C)]
332
#[derive(Debug, Default, Clone, Copy, Pod, Zeroable, ShaderType)]
333
pub struct GpuParticleGroup {
334
    /// The index of this particle group in the global particle group buffer.
335
    pub global_group_index: u32,
336
    /// The global index of the entire particle effect.
337
    pub effect_index: u32,
338
    /// The index of this effect in the group.
339
    ///
340
    /// For example, the first group in an effect has index 0, the second has
341
    /// index 1, etc.
342
    pub group_index_in_effect: u32,
343
    /// The index of the first particle in this group in the indirect index
344
    /// buffer.
345
    pub indirect_index: u32,
346
    /// The capacity of this group in number of particles.
347
    pub capacity: u32,
348
    // The index of the first particle in this effect in the particle and
349
    // indirect buffers.
350
    pub effect_particle_offset: u32,
351
}
352

353
/// Compute pipeline to run the `vfx_indirect` dispatch workgroup calculation
354
/// shader.
355
#[derive(Resource)]
356
pub(crate) struct DispatchIndirectPipeline {
357
    dispatch_indirect_layout: BindGroupLayout,
358
    pipeline: ComputePipeline,
359
}
360

361
impl FromWorld for DispatchIndirectPipeline {
362
    fn from_world(world: &mut World) -> Self {
1✔
363
        let render_device = world.get_resource::<RenderDevice>().unwrap();
1✔
364

365
        let storage_alignment = render_device.limits().min_storage_buffer_offset_alignment;
1✔
366
        let render_effect_indirect_size = GpuRenderEffectMetadata::aligned_size(storage_alignment);
1✔
367
        let render_group_indirect_size = GpuRenderGroupIndirect::aligned_size(storage_alignment);
1✔
368
        let dispatch_indirect_size = GpuDispatchIndirect::aligned_size(storage_alignment);
1✔
369
        let particle_group_size = GpuParticleGroup::aligned_size(storage_alignment);
1✔
370

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

439
        trace!("GpuSimParams: min_size={}", GpuSimParams::min_size());
1✔
440
        let sim_params_layout = render_device.create_bind_group_layout(
1✔
441
            "hanabi:bind_group_layout:dispatch_indirect_sim_params",
442
            &[BindGroupLayoutEntry {
1✔
443
                binding: 0,
1✔
444
                visibility: ShaderStages::COMPUTE,
1✔
445
                ty: BindingType::Buffer {
1✔
446
                    ty: BufferBindingType::Uniform,
1✔
447
                    has_dynamic_offset: false,
1✔
448
                    min_binding_size: Some(GpuSimParams::min_size()),
1✔
449
                },
450
                count: None,
1✔
451
            }],
452
        );
453

454
        let pipeline_layout = render_device.create_pipeline_layout(&PipelineLayoutDescriptor {
1✔
455
            label: Some("hanabi:pipeline_layout:dispatch_indirect"),
1✔
456
            bind_group_layouts: &[&dispatch_indirect_layout, &sim_params_layout],
1✔
457
            push_constant_ranges: &[],
1✔
458
        });
459

460
        let render_effect_indirect_stride_code =
1✔
461
            (render_effect_indirect_size.get() as u32).to_wgsl_string();
1✔
462
        let render_group_indirect_stride_code =
1✔
463
            (render_group_indirect_size.get() as u32).to_wgsl_string();
1✔
464
        let dispatch_indirect_stride_code = (dispatch_indirect_size.get() as u32).to_wgsl_string();
1✔
465
        let indirect_code = include_str!("vfx_indirect.wgsl")
1✔
466
            .replace(
467
                "{{RENDER_EFFECT_INDIRECT_STRIDE}}",
468
                &render_effect_indirect_stride_code,
1✔
469
            )
470
            .replace(
471
                "{{RENDER_GROUP_INDIRECT_STRIDE}}",
472
                &render_group_indirect_stride_code,
1✔
473
            )
474
            .replace(
475
                "{{DISPATCH_INDIRECT_STRIDE}}",
476
                &dispatch_indirect_stride_code,
1✔
477
            );
478

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

484
            // Import bevy_hanabi::vfx_common
485
            {
486
                let common_shader = HanabiPlugin::make_common_shader(
487
                    render_device.limits().min_storage_buffer_offset_alignment,
488
                );
489
                let mut desc: naga_oil::compose::ComposableModuleDescriptor<'_> =
490
                    (&common_shader).into();
491
                desc.shader_defs.insert(
492
                    "SPAWNER_PADDING".to_string(),
493
                    naga_oil::compose::ShaderDefValue::Bool(true),
494
                );
495
                let res = composer.add_composable_module(desc);
496
                assert!(res.is_ok());
497
            }
498

499
            let shader_defs = default();
1✔
500

501
            match composer.make_naga_module(NagaModuleDescriptor {
1✔
502
                source: &indirect_code,
1✔
503
                file_path: "vfx_indirect.wgsl",
1✔
504
                shader_defs,
1✔
505
                ..Default::default()
1✔
506
            }) {
507
                Ok(naga_module) => ShaderSource::Naga(Cow::Owned(naga_module)),
508
                Err(compose_error) => panic!(
×
509
                    "Failed to compose vfx_indirect.wgsl, naga_oil returned: {}",
510
                    compose_error.emit_to_string(&composer)
×
511
                ),
512
            }
513
        };
514

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

517
        let shader_module = render_device.create_shader_module(ShaderModuleDescriptor {
1✔
518
            label: Some("hanabi:vfx_indirect_shader"),
1✔
519
            source: indirect_naga_module,
1✔
520
        });
521

522
        let pipeline = render_device.create_compute_pipeline(&RawComputePipelineDescriptor {
1✔
523
            label: Some("hanabi:compute_pipeline:dispatch_indirect"),
1✔
524
            layout: Some(&pipeline_layout),
1✔
525
            module: &shader_module,
1✔
526
            entry_point: "main",
1✔
527
            compilation_options: default(),
1✔
528
        });
529

530
        Self {
531
            dispatch_indirect_layout,
532
            pipeline,
533
        }
534
    }
535
}
536

537
#[derive(Resource)]
538
pub(crate) struct ParticlesInitPipeline {
539
    render_device: RenderDevice,
540
    sim_params_layout: BindGroupLayout,
541
    spawner_buffer_layout: BindGroupLayout,
542
    render_indirect_layout: BindGroupLayout,
543
}
544

545
#[derive(Clone, PartialEq, Eq, Hash)]
546
pub(crate) struct ParticleInitPipelineKey {
547
    shader: Handle<Shader>,
548
    particle_layout_min_binding_size: NonZero<u64>,
549
    property_layout_min_binding_size: Option<NonZero<u64>>,
550
    flags: ParticleInitPipelineKeyFlags,
551
}
552

553
bitflags! {
554
    #[derive(Clone, Copy, PartialEq, Eq, Hash)]
555
    pub struct ParticleInitPipelineKeyFlags: u8 {
556
        const CLONE = 0x1;
557
        const ATTRIBUTE_PREV = 0x2;
558
        const ATTRIBUTE_NEXT = 0x4;
559
    }
560
}
561

562
impl FromWorld for ParticlesInitPipeline {
563
    fn from_world(world: &mut World) -> Self {
1✔
564
        let render_device = world.get_resource::<RenderDevice>().unwrap();
1✔
565

566
        let sim_params_layout = render_device.create_bind_group_layout(
1✔
567
            "hanabi:bind_group_layout:update_sim_params",
568
            &[BindGroupLayoutEntry {
1✔
569
                binding: 0,
1✔
570
                visibility: ShaderStages::COMPUTE,
1✔
571
                ty: BindingType::Buffer {
1✔
572
                    ty: BufferBindingType::Uniform,
1✔
573
                    has_dynamic_offset: false,
1✔
574
                    min_binding_size: Some(GpuSimParams::min_size()),
1✔
575
                },
576
                count: None,
1✔
577
            }],
578
        );
579

580
        let spawner_buffer_layout = render_device.create_bind_group_layout(
1✔
581
            "hanabi:buffer_layout:init_spawner",
582
            &[BindGroupLayoutEntry {
1✔
583
                binding: 0,
1✔
584
                visibility: ShaderStages::COMPUTE,
1✔
585
                ty: BindingType::Buffer {
1✔
586
                    ty: BufferBindingType::Storage { read_only: false },
1✔
587
                    has_dynamic_offset: true,
1✔
588
                    min_binding_size: Some(GpuSpawnerParams::min_size()),
1✔
589
                },
590
                count: None,
1✔
591
            }],
592
        );
593

594
        let render_indirect_layout = create_init_render_indirect_bind_group_layout(
595
            render_device,
1✔
596
            "hanabi:bind_group_layout:init_render_indirect",
597
            true,
598
        );
599

600
        Self {
601
            render_device: render_device.clone(),
1✔
602
            sim_params_layout,
603
            spawner_buffer_layout,
604
            render_indirect_layout,
605
        }
606
    }
607
}
608

609
impl SpecializedComputePipeline for ParticlesInitPipeline {
610
    type Key = ParticleInitPipelineKey;
611

612
    fn specialize(&self, key: Self::Key) -> ComputePipelineDescriptor {
×
613
        let particles_buffer_layout = create_init_particles_bind_group_layout(
NEW
614
            &self.render_device,
×
615
            "hanabi:init_particles_buffer_layout",
NEW
616
            key.particle_layout_min_binding_size,
×
NEW
617
            key.property_layout_min_binding_size,
×
618
        );
619

NEW
620
        let mut shader_defs = vec![];
×
NEW
621
        if key.flags.contains(ParticleInitPipelineKeyFlags::CLONE) {
×
NEW
622
            shader_defs.push(ShaderDefVal::Bool("CLONE".to_string(), true));
×
623
        }
NEW
624
        if key
×
NEW
625
            .flags
×
NEW
626
            .contains(ParticleInitPipelineKeyFlags::ATTRIBUTE_PREV)
×
627
        {
NEW
628
            shader_defs.push(ShaderDefVal::Bool("ATTRIBUTE_PREV".to_string(), true));
×
629
        }
NEW
630
        if key
×
NEW
631
            .flags
×
NEW
632
            .contains(ParticleInitPipelineKeyFlags::ATTRIBUTE_NEXT)
×
633
        {
NEW
634
            shader_defs.push(ShaderDefVal::Bool("ATTRIBUTE_NEXT".to_string(), true));
×
635
        }
636

637
        ComputePipelineDescriptor {
638
            label: Some("hanabi:pipeline_init_compute".into()),
×
639
            layout: vec![
×
640
                self.sim_params_layout.clone(),
641
                particles_buffer_layout,
642
                self.spawner_buffer_layout.clone(),
643
                self.render_indirect_layout.clone(),
644
            ],
645
            shader: key.shader,
×
646
            shader_defs,
647
            entry_point: "main".into(),
×
NEW
648
            push_constant_ranges: vec![],
×
649
        }
650
    }
651
}
652

653
#[derive(Resource)]
654
pub(crate) struct ParticlesUpdatePipeline {
655
    render_device: RenderDevice,
656
    sim_params_layout: BindGroupLayout,
657
    spawner_buffer_layout: BindGroupLayout,
658
    render_indirect_layout: BindGroupLayout,
659
}
660

661
impl FromWorld for ParticlesUpdatePipeline {
662
    fn from_world(world: &mut World) -> Self {
1✔
663
        let render_device = world.get_resource::<RenderDevice>().unwrap();
1✔
664

665
        let limits = render_device.limits();
1✔
666
        bevy::log::info!(
1✔
667
            "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✔
668
            limits.max_compute_invocations_per_workgroup, limits.max_compute_workgroup_size_x, limits.max_compute_workgroup_size_y, limits.max_compute_workgroup_size_z,
669
            limits.max_compute_workgroups_per_dimension, limits.min_storage_buffer_offset_alignment
670
        );
671

672
        trace!("GpuSimParams: min_size={}", GpuSimParams::min_size());
1✔
673
        let sim_params_layout = render_device.create_bind_group_layout(
1✔
674
            "hanabi:update_sim_params_layout",
675
            &[BindGroupLayoutEntry {
1✔
676
                binding: 0,
1✔
677
                visibility: ShaderStages::COMPUTE,
1✔
678
                ty: BindingType::Buffer {
1✔
679
                    ty: BufferBindingType::Uniform,
1✔
680
                    has_dynamic_offset: false,
1✔
681
                    min_binding_size: Some(GpuSimParams::min_size()),
1✔
682
                },
683
                count: None,
1✔
684
            }],
685
        );
686

687
        trace!(
1✔
688
            "GpuSpawnerParams: min_size={}",
×
689
            GpuSpawnerParams::min_size()
×
690
        );
691
        let spawner_buffer_layout = render_device.create_bind_group_layout(
1✔
692
            "hanabi:update_spawner_buffer_layout",
693
            &[BindGroupLayoutEntry {
1✔
694
                binding: 0,
1✔
695
                visibility: ShaderStages::COMPUTE,
1✔
696
                ty: BindingType::Buffer {
1✔
697
                    ty: BufferBindingType::Storage { read_only: false },
1✔
698
                    has_dynamic_offset: true,
1✔
699
                    min_binding_size: Some(GpuSpawnerParams::min_size()),
1✔
700
                },
701
                count: None,
1✔
702
            }],
703
        );
704

705
        let storage_alignment = render_device.limits().min_storage_buffer_offset_alignment;
1✔
706
        let render_effect_indirect_size = GpuRenderEffectMetadata::aligned_size(storage_alignment);
1✔
707
        let render_group_indirect_size = GpuRenderGroupIndirect::aligned_size(storage_alignment);
1✔
708
        trace!("GpuRenderEffectMetadata: min_size={} padded_size={} | GpuRenderGroupIndirect: min_size={} padded_size={}",
1✔
709
            GpuRenderEffectMetadata::min_size(),
×
710
            render_effect_indirect_size.get(),
×
711
            GpuRenderGroupIndirect::min_size(),
×
712
            render_group_indirect_size.get());
×
713
        let render_indirect_layout = render_device.create_bind_group_layout(
1✔
714
            "hanabi:update_render_indirect_layout",
715
            &[
1✔
716
                BindGroupLayoutEntry {
1✔
717
                    binding: 0,
1✔
718
                    visibility: ShaderStages::COMPUTE,
1✔
719
                    ty: BindingType::Buffer {
1✔
720
                        ty: BufferBindingType::Storage { read_only: false },
1✔
721
                        has_dynamic_offset: false,
1✔
722
                        min_binding_size: Some(render_effect_indirect_size),
1✔
723
                    },
724
                    count: None,
1✔
725
                },
726
                BindGroupLayoutEntry {
1✔
727
                    binding: 1,
1✔
728
                    visibility: ShaderStages::COMPUTE,
1✔
729
                    ty: BindingType::Buffer {
1✔
730
                        ty: BufferBindingType::Storage { read_only: false },
1✔
731
                        has_dynamic_offset: false,
1✔
732
                        // Array; needs padded size
733
                        min_binding_size: Some(render_group_indirect_size),
1✔
734
                    },
735
                    count: None,
1✔
736
                },
737
            ],
738
        );
739

740
        Self {
741
            render_device: render_device.clone(),
1✔
742
            sim_params_layout,
743
            spawner_buffer_layout,
744
            render_indirect_layout,
745
        }
746
    }
747
}
748

749
#[derive(Default, Clone, Hash, PartialEq, Eq)]
750
pub(crate) struct ParticleUpdatePipelineKey {
751
    /// Compute shader, with snippets applied, but not preprocessed yet.
752
    shader: Handle<Shader>,
753
    /// Particle layout.
754
    particle_layout: ParticleLayout,
755
    /// Property layout.
756
    property_layout: PropertyLayout,
757
    is_trail: bool,
758
}
759

760
impl SpecializedComputePipeline for ParticlesUpdatePipeline {
761
    type Key = ParticleUpdatePipelineKey;
762

763
    fn specialize(&self, key: Self::Key) -> ComputePipelineDescriptor {
×
764
        trace!(
×
765
            "GpuParticle: attributes.min_binding_size={} properties.min_binding_size={}",
×
766
            key.particle_layout.min_binding_size().get(),
×
767
            if key.property_layout.is_empty() {
×
768
                0
×
769
            } else {
770
                key.property_layout.min_binding_size().get()
×
771
            },
772
        );
773

774
        let update_particles_buffer_layout = create_update_bind_group_layout(
775
            &self.render_device,
776
            "hanabi:update_particles_buffer_layout",
777
            key.particle_layout.min_binding_size(),
778
            if key.property_layout.is_empty() {
NEW
779
                None
×
780
            } else {
NEW
781
                Some(key.property_layout.min_binding_size())
×
782
            },
783
        );
784

785
        let mut shader_defs = vec!["REM_MAX_SPAWN_ATOMIC".into()];
NEW
786
        if key.particle_layout.contains(Attribute::PREV) {
×
NEW
787
            shader_defs.push("ATTRIBUTE_PREV".into());
×
788
        }
NEW
789
        if key.particle_layout.contains(Attribute::NEXT) {
×
NEW
790
            shader_defs.push("ATTRIBUTE_NEXT".into());
×
791
        }
NEW
792
        if key.is_trail {
×
NEW
793
            shader_defs.push("TRAIL".into());
×
794
        }
795

796
        ComputePipelineDescriptor {
797
            label: Some("hanabi:pipeline_update_compute".into()),
798
            layout: vec![
799
                self.sim_params_layout.clone(),
800
                update_particles_buffer_layout,
801
                self.spawner_buffer_layout.clone(),
802
                self.render_indirect_layout.clone(),
803
            ],
804
            shader: key.shader,
805
            shader_defs,
806
            entry_point: "main".into(),
807
            push_constant_ranges: Vec::new(),
808
        }
809
    }
810
}
811

812
#[derive(Resource)]
813
pub(crate) struct ParticlesRenderPipeline {
814
    render_device: RenderDevice,
815
    view_layout: BindGroupLayout,
816
    material_layouts: HashMap<TextureLayout, BindGroupLayout>,
817
}
818

819
impl ParticlesRenderPipeline {
820
    /// Cache a material, creating its bind group layout based on the texture
821
    /// layout.
822
    pub fn cache_material(&mut self, layout: &TextureLayout) {
×
823
        if layout.layout.is_empty() {
×
824
            return;
×
825
        }
826

827
        // FIXME - no current stable API to insert an entry into a HashMap only if it
828
        // doesn't exist, and without having to build a key (as opposed to a reference).
829
        // So do 2 lookups instead, to avoid having to clone the layout if it's already
830
        // cached (which should be the common case).
831
        if self.material_layouts.contains_key(layout) {
×
832
            return;
×
833
        }
834

835
        let mut entries = Vec::with_capacity(layout.layout.len() * 2);
×
836
        let mut index = 0;
×
837
        for _slot in &layout.layout {
×
838
            entries.push(BindGroupLayoutEntry {
×
839
                binding: index,
×
840
                visibility: ShaderStages::FRAGMENT,
×
841
                ty: BindingType::Texture {
×
842
                    multisampled: false,
×
843
                    sample_type: TextureSampleType::Float { filterable: true },
×
844
                    view_dimension: TextureViewDimension::D2,
×
845
                },
846
                count: None,
×
847
            });
848
            entries.push(BindGroupLayoutEntry {
×
849
                binding: index + 1,
×
850
                visibility: ShaderStages::FRAGMENT,
×
851
                ty: BindingType::Sampler(SamplerBindingType::Filtering),
×
852
                count: None,
×
853
            });
854
            index += 2;
×
855
        }
856
        let material_bind_group_layout = self
×
857
            .render_device
×
858
            .create_bind_group_layout("hanabi:material_layout_render", &entries[..]);
×
859

860
        self.material_layouts
×
861
            .insert(layout.clone(), material_bind_group_layout);
×
862
    }
863

864
    /// Retrieve a bind group layout for a cached material.
865
    pub fn get_material(&self, layout: &TextureLayout) -> Option<&BindGroupLayout> {
×
866
        // Prevent a hash and lookup for the trivial case of an empty layout
867
        if layout.layout.is_empty() {
×
868
            return None;
×
869
        }
870

871
        self.material_layouts.get(layout)
×
872
    }
873
}
874

875
impl FromWorld for ParticlesRenderPipeline {
876
    fn from_world(world: &mut World) -> Self {
1✔
877
        let render_device = world.get_resource::<RenderDevice>().unwrap();
1✔
878

879
        let view_layout = render_device.create_bind_group_layout(
1✔
880
            "hanabi:view_layout_render",
881
            &[
1✔
882
                BindGroupLayoutEntry {
1✔
883
                    binding: 0,
1✔
884
                    visibility: ShaderStages::VERTEX_FRAGMENT,
1✔
885
                    ty: BindingType::Buffer {
1✔
886
                        ty: BufferBindingType::Uniform,
1✔
887
                        has_dynamic_offset: true,
1✔
888
                        min_binding_size: Some(ViewUniform::min_size()),
1✔
889
                    },
890
                    count: None,
1✔
891
                },
892
                BindGroupLayoutEntry {
1✔
893
                    binding: 1,
1✔
894
                    visibility: ShaderStages::VERTEX_FRAGMENT,
1✔
895
                    ty: BindingType::Buffer {
1✔
896
                        ty: BufferBindingType::Uniform,
1✔
897
                        has_dynamic_offset: false,
1✔
898
                        min_binding_size: Some(GpuSimParams::min_size()),
1✔
899
                    },
900
                    count: None,
1✔
901
                },
902
            ],
903
        );
904

905
        Self {
906
            render_device: render_device.clone(),
1✔
907
            view_layout,
908
            material_layouts: default(),
1✔
909
        }
910
    }
911
}
912

913
#[cfg(all(feature = "2d", feature = "3d"))]
914
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
915
enum PipelineMode {
916
    Camera2d,
917
    Camera3d,
918
}
919

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

958
impl Default for ParticleRenderPipelineKey {
959
    fn default() -> Self {
×
960
        Self {
961
            shader: Handle::default(),
×
962
            particle_layout: ParticleLayout::empty(),
×
963
            texture_layout: default(),
×
964
            local_space_simulation: false,
965
            use_alpha_mask: false,
966
            alpha_mode: AlphaMode::Blend,
967
            flipbook: false,
968
            needs_uv: false,
969
            ribbons: false,
970
            #[cfg(all(feature = "2d", feature = "3d"))]
971
            pipeline_mode: PipelineMode::Camera3d,
972
            msaa_samples: Msaa::default().samples(),
×
973
            hdr: false,
974
        }
975
    }
976
}
977

978
impl SpecializedRenderPipeline for ParticlesRenderPipeline {
979
    type Key = ParticleRenderPipelineKey;
980

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

984
        // Base mandatory part of vertex buffer layout
985
        let vertex_buffer_layout = VertexBufferLayout {
986
            array_stride: 20,
987
            step_mode: VertexStepMode::Vertex,
988
            attributes: vec![
×
989
                //  @location(0) vertex_position: vec3<f32>
990
                VertexAttribute {
991
                    format: VertexFormat::Float32x3,
992
                    offset: 0,
993
                    shader_location: 0,
994
                },
995
                //  @location(1) vertex_uv: vec2<f32>
996
                VertexAttribute {
997
                    format: VertexFormat::Float32x2,
998
                    offset: 12,
999
                    shader_location: 1,
1000
                },
1001
                //  @location(1) vertex_color: u32
1002
                // VertexAttribute {
1003
                //     format: VertexFormat::Uint32,
1004
                //     offset: 12,
1005
                //     shader_location: 1,
1006
                // },
1007
                //  @location(2) vertex_velocity: vec3<f32>
1008
                // VertexAttribute {
1009
                //     format: VertexFormat::Float32x3,
1010
                //     offset: 12,
1011
                //     shader_location: 1,
1012
                // },
1013
                //  @location(3) vertex_uv: vec2<f32>
1014
                // VertexAttribute {
1015
                //     format: VertexFormat::Float32x2,
1016
                //     offset: 28,
1017
                //     shader_location: 3,
1018
                // },
1019
            ],
1020
        };
1021

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

1072
        trace!(
1073
            "GpuParticle: layout.min_binding_size={}",
×
1074
            key.particle_layout.min_binding_size()
×
1075
        );
1076
        trace!(
×
1077
            "Creating render bind group layout with {} entries",
×
1078
            entries.len()
×
1079
        );
1080
        let particles_buffer_layout = self
×
1081
            .render_device
×
1082
            .create_bind_group_layout("hanabi:buffer_layout_render", &entries);
×
1083

1084
        let mut layout = vec![self.view_layout.clone(), particles_buffer_layout];
×
1085
        let mut shader_defs = vec!["SPAWNER_READONLY".into()];
×
1086

1087
        if let Some(material_bind_group_layout) = self.get_material(&key.texture_layout) {
×
1088
            layout.push(material_bind_group_layout.clone());
1089
            // //  @location(1) vertex_uv: vec2<f32>
1090
            // vertex_buffer_layout.attributes.push(VertexAttribute {
1091
            //     format: VertexFormat::Float32x2,
1092
            //     offset: 12,
1093
            //     shader_location: 1,
1094
            // });
1095
            // vertex_buffer_layout.array_stride += 8;
1096
        }
1097

1098
        // Key: LOCAL_SPACE_SIMULATION
1099
        if key.local_space_simulation {
×
1100
            shader_defs.push("LOCAL_SPACE_SIMULATION".into());
×
1101
            shader_defs.push("RENDER_NEEDS_SPAWNER".into());
×
1102
        }
1103

1104
        // Key: USE_ALPHA_MASK
1105
        if key.use_alpha_mask {
×
1106
            shader_defs.push("USE_ALPHA_MASK".into());
×
1107
        }
1108

1109
        // Key: FLIPBOOK
1110
        if key.flipbook {
×
1111
            shader_defs.push("FLIPBOOK".into());
×
1112
        }
1113

1114
        // Key: NEEDS_UV
1115
        if key.needs_uv {
×
1116
            shader_defs.push("NEEDS_UV".into());
×
1117
        }
1118

1119
        // Key: RIBBONS
NEW
1120
        if key.ribbons {
×
NEW
1121
            shader_defs.push("RIBBONS".into());
×
1122
        }
1123

1124
        #[cfg(all(feature = "2d", feature = "3d"))]
1125
        let depth_stencil = match key.pipeline_mode {
1126
            // Bevy's Transparent2d render phase doesn't support a depth-stencil buffer.
1127
            PipelineMode::Camera2d => None,
×
1128
            PipelineMode::Camera3d => Some(DepthStencilState {
×
1129
                format: TextureFormat::Depth32Float,
×
1130
                // Use depth buffer with alpha-masked particles, not with transparent ones
1131
                depth_write_enabled: key.use_alpha_mask,
×
1132
                // Bevy uses reverse-Z, so Greater really means closer
1133
                depth_compare: CompareFunction::Greater,
×
1134
                stencil: StencilState::default(),
×
1135
                bias: DepthBiasState::default(),
×
1136
            }),
1137
        };
1138

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

1142
        #[cfg(all(feature = "3d", not(feature = "2d")))]
1143
        let depth_stencil = Some(DepthStencilState {
1144
            format: TextureFormat::Depth32Float,
1145
            // Use depth buffer with alpha-masked particles, not with transparent ones
1146
            depth_write_enabled: key.use_alpha_mask,
1147
            // Bevy uses reverse-Z, so Greater really means closer
1148
            depth_compare: CompareFunction::Greater,
1149
            stencil: StencilState::default(),
1150
            bias: DepthBiasState::default(),
1151
        });
1152

1153
        let format = if key.hdr {
1154
            ViewTarget::TEXTURE_FORMAT_HDR
×
1155
        } else {
1156
            TextureFormat::bevy_default()
×
1157
        };
1158

1159
        let blend_state = match key.alpha_mode {
×
1160
            AlphaMode::Blend => BlendState::ALPHA_BLENDING,
×
1161
            AlphaMode::Premultiply => BlendState::PREMULTIPLIED_ALPHA_BLENDING,
×
1162
            AlphaMode::Add => BlendState {
1163
                color: BlendComponent {
×
1164
                    src_factor: BlendFactor::SrcAlpha,
1165
                    dst_factor: BlendFactor::One,
1166
                    operation: BlendOperation::Add,
1167
                },
1168
                alpha: BlendComponent {
×
1169
                    src_factor: BlendFactor::Zero,
1170
                    dst_factor: BlendFactor::One,
1171
                    operation: BlendOperation::Add,
1172
                },
1173
            },
1174
            AlphaMode::Multiply => BlendState {
1175
                color: BlendComponent {
×
1176
                    src_factor: BlendFactor::Dst,
1177
                    dst_factor: BlendFactor::OneMinusSrcAlpha,
1178
                    operation: BlendOperation::Add,
1179
                },
1180
                alpha: BlendComponent::OVER,
1181
            },
1182
            _ => BlendState::ALPHA_BLENDING,
×
1183
        };
1184

1185
        RenderPipelineDescriptor {
1186
            vertex: VertexState {
1187
                shader: key.shader.clone(),
1188
                entry_point: "vertex".into(),
1189
                shader_defs: shader_defs.clone(),
1190
                buffers: vec![vertex_buffer_layout],
1191
            },
1192
            fragment: Some(FragmentState {
1193
                shader: key.shader,
1194
                shader_defs,
1195
                entry_point: "fragment".into(),
1196
                targets: vec![Some(ColorTargetState {
1197
                    format,
1198
                    blend: Some(blend_state),
1199
                    write_mask: ColorWrites::ALL,
1200
                })],
1201
            }),
1202
            layout,
1203
            primitive: PrimitiveState {
1204
                front_face: FrontFace::Ccw,
1205
                cull_mode: None,
1206
                unclipped_depth: false,
1207
                polygon_mode: PolygonMode::Fill,
1208
                conservative: false,
1209
                topology: PrimitiveTopology::TriangleList,
1210
                strip_index_format: None,
1211
            },
1212
            depth_stencil,
1213
            multisample: MultisampleState {
1214
                count: key.msaa_samples,
1215
                mask: !0,
1216
                alpha_to_coverage_enabled: false,
1217
            },
1218
            label: Some("hanabi:pipeline_render".into()),
1219
            push_constant_ranges: Vec::new(),
1220
        }
1221
    }
1222
}
1223

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

1276
/// Extracted data for newly-added [`ParticleEffect`] component requiring a new
1277
/// GPU allocation.
1278
///
1279
/// [`ParticleEffect`]: crate::ParticleEffect
1280
pub struct AddedEffect {
1281
    /// Entity with a newly-added [`ParticleEffect`] component.
1282
    ///
1283
    /// [`ParticleEffect`]: crate::ParticleEffect
1284
    pub entity: Entity,
1285
    pub groups: Vec<AddedEffectGroup>,
1286
    /// Layout of particle attributes.
1287
    pub particle_layout: ParticleLayout,
1288
    /// Layout of properties for the effect, if properties are used at all, or
1289
    /// an empty layout.
1290
    pub property_layout: PropertyLayout,
1291
    pub layout_flags: LayoutFlags,
1292
    /// Handle of the effect asset.
1293
    pub handle: Handle<EffectAsset>,
1294
    /// The order in which we evaluate groups.
1295
    pub group_order: Vec<u32>,
1296
}
1297

1298
pub struct AddedEffectGroup {
1299
    pub capacity: u32,
1300
    pub src_group_index_if_trail: Option<u32>,
1301
}
1302

1303
/// Collection of all extracted effects for this frame, inserted into the
1304
/// render world as a render resource.
1305
#[derive(Default, Resource)]
1306
pub(crate) struct ExtractedEffects {
1307
    /// Map of extracted effects from the entity the source [`ParticleEffect`]
1308
    /// is on.
1309
    ///
1310
    /// [`ParticleEffect`]: crate::ParticleEffect
1311
    pub effects: HashMap<Entity, ExtractedEffect>,
1312
    /// Entites which had their [`ParticleEffect`] component removed.
1313
    ///
1314
    /// [`ParticleEffect`]: crate::ParticleEffect
1315
    pub removed_effect_entities: Vec<Entity>,
1316
    /// Newly added effects without a GPU allocation yet.
1317
    pub added_effects: Vec<AddedEffect>,
1318
}
1319

1320
#[derive(Default, Resource)]
1321
pub(crate) struct EffectAssetEvents {
1322
    pub images: Vec<AssetEvent<Image>>,
1323
}
1324

1325
/// System extracting all the asset events for the [`Image`] assets to enable
1326
/// dynamic update of images bound to any effect.
1327
///
1328
/// This system runs in parallel of [`extract_effects`].
1329
pub(crate) fn extract_effect_events(
10✔
1330
    mut events: ResMut<EffectAssetEvents>,
1331
    mut image_events: Extract<EventReader<AssetEvent<Image>>>,
1332
) {
1333
    trace!("extract_effect_events");
10✔
1334

1335
    let EffectAssetEvents { ref mut images } = *events;
10✔
1336
    *images = image_events.read().copied().collect();
10✔
1337
}
1338

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

1381
    // Save simulation params into render world
1382
    sim_params.time = time.elapsed_seconds_f64();
10✔
1383
    sim_params.delta_time = time.delta_seconds();
10✔
1384
    sim_params.virtual_time = virtual_time.elapsed_seconds_f64();
10✔
1385
    sim_params.virtual_delta_time = virtual_time.delta_seconds();
10✔
1386
    sim_params.real_time = real_time.elapsed_seconds_f64();
10✔
1387
    sim_params.real_delta_time = real_time.delta_seconds();
10✔
1388

1389
    // Collect removed effects for later GPU data purge
1390
    extracted_effects.removed_effect_entities =
10✔
1391
        removed_effects_event_reader
10✔
1392
            .read()
10✔
1393
            .fold(vec![], |mut acc, ev| {
10✔
1394
                // FIXME - Need to clone because we can't consume the event, we only have
1395
                // read-only access to the main world
1396
                acc.append(&mut ev.entities.clone());
×
1397
                acc
×
1398
            });
1399
    trace!(
1400
        "Found {} removed entities.",
×
1401
        extracted_effects.removed_effect_entities.len()
×
1402
    );
1403

1404
    // Collect added effects for later GPU data allocation
1405
    extracted_effects.added_effects = query
10✔
1406
        .p1()
10✔
1407
        .iter()
10✔
1408
        .filter_map(|(entity, effect)| {
11✔
1409
            let handle = effect.asset.clone_weak();
1✔
1410
            let asset = effects.get(&effect.asset)?;
2✔
1411
            let particle_layout = asset.particle_layout();
1412
            assert!(
1413
                particle_layout.size() > 0,
1414
                "Invalid empty particle layout for effect '{}' on entity {:?}. Did you forget to add some modifier to the asset?",
×
1415
                asset.name,
1416
                entity
1417
            );
1418
            let property_layout = asset.property_layout();
×
NEW
1419
            let group_order = asset.calculate_group_order();
×
1420

NEW
1421
            trace!(
×
NEW
1422
                "Found new effect: entity {:?} | capacities {:?} | particle_layout {:?} | \
×
NEW
1423
                 property_layout {:?} | layout_flags {:?}",
×
NEW
1424
                 entity,
×
NEW
1425
                 asset.capacities(),
×
1426
                 particle_layout,
1427
                 property_layout,
1428
                 effect.layout_flags);
1429

1430
            Some(AddedEffect {
×
1431
                entity,
×
NEW
1432
                groups: asset.capacities().iter().zip(asset.init.iter()).map(|(&capacity, init)| {
×
NEW
1433
                    AddedEffectGroup {
×
NEW
1434
                        capacity,
×
NEW
1435
                        src_group_index_if_trail: match init {
×
NEW
1436
                            Initializer::Spawner(_) => None,
×
NEW
1437
                            Initializer::Cloner(cloner) => Some(cloner.src_group_index),
×
1438
                        }
1439
                    }
NEW
1440
                }).collect(),
×
1441
                particle_layout,
1442
                property_layout,
1443
                group_order,
1444
                layout_flags: effect.layout_flags,
1445
                handle,
1446
            })
1447
        })
1448
        .collect();
1449

1450
    // Loop over all existing effects to update them
1451
    extracted_effects.effects.clear();
1452
    for (
1453
        entity,
×
1454
        maybe_inherited_visibility,
×
1455
        maybe_view_visibility,
×
NEW
1456
        initializers,
×
1457
        effect,
×
1458
        maybe_properties,
×
1459
        transform,
×
1460
    ) in query.p0().iter_mut()
1461
    {
1462
        // Check if shaders are configured
NEW
1463
        let effect_shaders = effect.get_configured_shaders().to_vec();
×
NEW
1464
        if effect_shaders.is_empty() {
×
UNCOV
1465
            continue;
×
1466
        }
1467

1468
        // Check if hidden, unless always simulated
UNCOV
1469
        if effect.simulation_condition == SimulationCondition::WhenVisible
×
1470
            && !maybe_inherited_visibility
×
1471
                .map(|cv| cv.get())
×
1472
                .unwrap_or(true)
×
1473
            && !maybe_view_visibility.map(|cv| cv.get()).unwrap_or(true)
×
1474
        {
1475
            continue;
×
1476
        }
1477

1478
        // Check if asset is available, otherwise silently ignore
1479
        let Some(asset) = effects.get(&effect.asset) else {
×
1480
            trace!(
×
1481
                "EffectAsset not ready; skipping ParticleEffect instance on entity {:?}.",
×
1482
                entity
1483
            );
1484
            continue;
×
1485
        };
1486

1487
        #[cfg(feature = "2d")]
1488
        let z_sort_key_2d = effect.z_layer_2d;
1489

1490
        let property_layout = asset.property_layout();
1491
        let texture_layout = asset.module().texture_layout();
1492

1493
        let property_data = if let Some(properties) = maybe_properties {
×
1494
            // Note: must check that property layout is not empty, because the
1495
            // EffectProperties component is marked as changed when added but contains an
1496
            // empty Vec if there's no property, which would later raise an error if we
1497
            // don't return None here.
1498
            if properties.is_changed() && !property_layout.is_empty() {
×
1499
                trace!("Detected property change, re-serializing...");
×
1500
                Some(properties.serialize(&property_layout))
×
1501
            } else {
1502
                None
×
1503
            }
1504
        } else {
1505
            None
×
1506
        };
1507

1508
        let layout_flags = effect.layout_flags;
1509
        let alpha_mode = effect.alpha_mode;
1510

1511
        trace!(
1512
            "Extracted instance of effect '{}' on entity {:?}: texture_layout_count={} texture_count={} layout_flags={:?}",
×
1513
            asset.name,
×
1514
            entity,
×
1515
            texture_layout.layout.len(),
×
1516
            effect.textures.len(),
×
1517
            layout_flags,
1518
        );
1519

1520
        extracted_effects.effects.insert(
×
1521
            entity,
×
1522
            ExtractedEffect {
×
1523
                handle: effect.asset.clone_weak(),
×
1524
                particle_layout: asset.particle_layout().clone(),
×
1525
                property_layout,
×
1526
                property_data,
×
NEW
1527
                initializers: initializers.0.clone(),
×
1528
                transform: transform.compute_matrix(),
×
1529
                // TODO - more efficient/correct way than inverse()?
1530
                inverse_transform: transform.compute_matrix().inverse(),
×
1531
                layout_flags,
×
1532
                texture_layout,
×
1533
                textures: effect.textures.clone(),
×
1534
                alpha_mode,
×
NEW
1535
                effect_shaders,
×
1536
                #[cfg(feature = "2d")]
×
1537
                z_sort_key_2d,
×
1538
            },
1539
        );
1540
    }
1541
}
1542

1543
/// GPU representation of a single vertex of a particle mesh stored in a GPU
1544
/// buffer.
1545
#[repr(C)]
1546
#[derive(Copy, Clone, Pod, Zeroable, ShaderType)]
1547
struct GpuParticleVertex {
1548
    /// Vertex position.
1549
    pub position: [f32; 3],
1550
    /// UV coordinates of vertex.
1551
    pub uv: [f32; 2],
1552
}
1553

1554
/// Various GPU limits and aligned sizes computed once and cached.
1555
struct GpuLimits {
1556
    /// Value of [`WgpuLimits::min_storage_buffer_offset_alignment`].
1557
    ///
1558
    /// [`WgpuLimits::min_storage_buffer_offset_alignment`]: bevy::render::settings::WgpuLimits::min_storage_buffer_offset_alignment
1559
    storage_buffer_align: NonZeroU32,
1560
    /// Size of [`GpuDispatchIndirect`] aligned to the contraint of
1561
    /// [`WgpuLimits::min_storage_buffer_offset_alignment`].
1562
    ///
1563
    /// [`WgpuLimits::min_storage_buffer_offset_alignment`]: bevy::render::settings::WgpuLimits::min_storage_buffer_offset_alignment
1564
    dispatch_indirect_aligned_size: NonZeroU32,
1565
    render_effect_indirect_aligned_size: NonZeroU32,
1566
    /// Size of [`GpuRenderIndirect`] aligned to the contraint of
1567
    /// [`WgpuLimits::min_storage_buffer_offset_alignment`].
1568
    ///
1569
    /// [`WgpuLimits::min_storage_buffer_offset_alignment`]: bevy::render::settings::WgpuLimits::min_storage_buffer_offset_alignment
1570
    render_group_indirect_aligned_size: NonZeroU32,
1571
    particle_group_aligned_size: NonZeroU32,
1572
}
1573

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

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

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

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

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

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

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

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

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

1630
    /// Byte alignment for [`GpuRenderEffectMetadata`].
1631
    pub fn render_effect_indirect_offset(&self, buffer_index: u32) -> u64 {
1✔
1632
        self.render_effect_indirect_aligned_size.get() as u64 * buffer_index as u64
1✔
1633
    }
1634
    pub fn render_effect_indirect_size(&self) -> NonZeroU64 {
×
1635
        NonZeroU64::new(self.render_effect_indirect_aligned_size.get() as u64).unwrap()
×
1636
    }
1637

1638
    /// Byte alignment for [`GpuRenderGroupIndirect`].
1639
    pub fn render_group_indirect_offset(&self, buffer_index: u32) -> u64 {
1✔
1640
        self.render_group_indirect_aligned_size.get() as u64 * buffer_index as u64
1✔
1641
    }
1642
    pub fn render_group_indirect_size(&self) -> NonZeroU64 {
×
1643
        NonZeroU64::new(self.render_group_indirect_aligned_size.get() as u64).unwrap()
×
1644
    }
1645

1646
    /// Byte alignment for [`GpuParticleGroup`].
1647
    pub fn particle_group_offset(&self, buffer_index: u32) -> u32 {
×
1648
        self.particle_group_aligned_size.get() * buffer_index
×
1649
    }
1650
}
1651

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

1656
/// Global resource containing the GPU data to draw all the particle effects in
1657
/// all views.
1658
///
1659
/// The resource is populated by [`prepare_effects()`] with all the effects to
1660
/// render for the current frame, for all views in the frame, and consumed by
1661
/// [`queue_effects()`] to actually enqueue the drawning commands to draw those
1662
/// effects.
1663
#[derive(Resource)]
1664
pub struct EffectsMeta {
1665
    /// Map from an entity with a [`ParticleEffect`] component attached to it,
1666
    /// to the associated effect slice allocated in the [`EffectCache`].
1667
    ///
1668
    /// [`ParticleEffect`]: crate::ParticleEffect
1669
    entity_map: HashMap<Entity, CacheEntry>,
1670
    /// Bind group for the camera view, containing the camera projection and
1671
    /// other uniform values related to the camera.
1672
    view_bind_group: Option<BindGroup>,
1673
    /// Bind group for the simulation parameters, like the current time and
1674
    /// frame delta time.
1675
    sim_params_bind_group: Option<BindGroup>,
1676
    /// Bind group for the spawning parameters (number of particles to spawn
1677
    /// this frame, ...).
1678
    spawner_bind_group: Option<BindGroup>,
1679
    /// Bind group #0 of the vfx_indirect shader, containing both the indirect
1680
    /// compute dispatch and render buffers.
1681
    dr_indirect_bind_group: Option<BindGroup>,
1682
    /// Bind group #3 of the vfx_init shader, containing the indirect render
1683
    /// buffer.
1684
    init_render_indirect_bind_group: Option<BindGroup>,
1685

1686
    sim_params_uniforms: UniformBuffer<GpuSimParams>,
1687
    spawner_buffer: AlignedBufferVec<GpuSpawnerParams>,
1688
    dispatch_indirect_buffer: BufferTable<GpuDispatchIndirect>,
1689
    /// Stores the GPU `RenderEffectMetadata` structures, which describe mutable
1690
    /// data relating to the entire effect.
1691
    render_effect_dispatch_buffer: BufferTable<GpuRenderEffectMetadata>,
1692
    /// Stores the GPU `RenderGroupIndirect` structures, which describe mutable
1693
    /// data specific to a particle group.
1694
    ///
1695
    /// These structures also store the data needed for indirect dispatch of
1696
    /// drawcalls.
1697
    render_group_dispatch_buffer: BufferTable<GpuRenderGroupIndirect>,
1698
    /// Stores the GPU `ParticleGroup` structures, which are metadata describing
1699
    /// each particle group that's populated by the CPU and read (only read) by
1700
    /// the GPU.
1701
    particle_group_buffer: AlignedBufferVec<GpuParticleGroup>,
1702
    /// Unscaled vertices of the mesh of a single particle, generally a quad.
1703
    /// The mesh is later scaled during rendering by the "particle size".
1704
    // FIXME - This is a per-effect thing, unless we merge all meshes into a single buffer (makes
1705
    // sense) but in that case we need a vertex slice too to know which mesh to draw per effect.
1706
    vertices: BufferVec<GpuParticleVertex>,
1707
    /// Various GPU limits and aligned sizes lazily allocated and cached for
1708
    /// convenience.
1709
    gpu_limits: GpuLimits,
1710
}
1711

1712
impl EffectsMeta {
1713
    pub fn new(device: RenderDevice) -> Self {
1✔
1714
        let mut vertices = BufferVec::new(BufferUsages::VERTEX);
1✔
1715
        for v in QUAD_VERTEX_POSITIONS {
19✔
1716
            let uv = v.truncate() + 0.5;
6✔
1717
            let v = *v * Vec3::new(1.0, 1.0, 1.0);
6✔
1718
            vertices.push(GpuParticleVertex {
6✔
1719
                position: v.into(),
6✔
1720
                uv: uv.into(),
6✔
1721
            });
1722
        }
1723

1724
        let gpu_limits = GpuLimits::from_device(&device);
1✔
1725

1726
        // Ensure individual GpuSpawnerParams elements are properly aligned so they can
1727
        // be addressed individually by the computer shaders.
1728
        let item_align = gpu_limits.storage_buffer_align().get() as u64;
1✔
1729
        trace!(
1✔
1730
            "Aligning storage buffers to {} bytes as device limits requires.",
×
1731
            item_align
1732
        );
1733

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

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

1811
                    let slices_ref = &cached_effect_indices.slices;
×
1812
                    debug_assert!(slices_ref.ranges.len() >= 2);
×
1813
                    let group_count = (slices_ref.ranges.len() - 1) as u32;
×
1814

1815
                    let first_row = slices_ref
×
1816
                        .dispatch_buffer_indices
×
1817
                        .first_update_group_dispatch_buffer_index
×
1818
                        .0;
×
1819
                    for table_id in first_row..(first_row + group_count) {
×
1820
                        self.dispatch_indirect_buffer
×
1821
                            .remove(BufferTableId(table_id));
×
1822
                    }
1823
                    self.render_effect_dispatch_buffer.remove(
×
1824
                        slices_ref
×
1825
                            .dispatch_buffer_indices
×
1826
                            .render_effect_metadata_buffer_index,
×
1827
                    );
1828
                    let first_row = slices_ref
×
1829
                        .dispatch_buffer_indices
×
1830
                        .first_render_group_dispatch_buffer_index
×
1831
                        .0;
×
1832
                    for table_id in first_row..(first_row + group_count) {
×
1833
                        self.render_group_dispatch_buffer
×
1834
                            .remove(BufferTableId(table_id));
×
1835
                    }
1836
                }
1837
            }
1838
        }
1839

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

1846
        trace!("Adding {} newly spawned effects", added_effects.len());
10✔
1847
        for added_effect in added_effects.drain(..) {
10✔
1848
            let first_update_group_dispatch_buffer_index = allocate_sequential_buffers(
UNCOV
1849
                &mut self.dispatch_indirect_buffer,
×
NEW
1850
                iter::repeat(GpuDispatchIndirect::default()).take(added_effect.groups.len()),
×
1851
            );
1852

NEW
1853
            let render_effect_dispatch_buffer_id = self
×
NEW
1854
                .render_effect_dispatch_buffer
×
NEW
1855
                .insert(GpuRenderEffectMetadata::default());
×
1856

1857
            let mut current_base_instance = 0;
×
1858
            let first_render_group_dispatch_buffer_index = allocate_sequential_buffers(
1859
                &mut self.render_group_dispatch_buffer,
×
NEW
1860
                added_effect.groups.iter().map(|group| {
×
1861
                    let indirect_dispatch = GpuRenderGroupIndirect {
×
1862
                        vertex_count: 6, // TODO - Flexible vertex count and mesh particles
×
NEW
1863
                        dead_count: group.capacity,
×
1864
                        base_instance: current_base_instance,
×
NEW
1865
                        max_spawn: group.capacity,
×
UNCOV
1866
                        ..default()
×
1867
                    };
NEW
1868
                    current_base_instance += group.capacity;
×
1869
                    indirect_dispatch
×
1870
                }),
1871
            );
1872

NEW
1873
            let mut trail_dispatch_buffer_indices = HashMap::new();
×
NEW
1874
            for (dest_group_index, group) in added_effect.groups.iter().enumerate() {
×
NEW
1875
                let Some(src_group_index) = group.src_group_index_if_trail else {
×
NEW
1876
                    continue;
×
1877
                };
1878
                trail_dispatch_buffer_indices.insert(
1879
                    dest_group_index as u32,
1880
                    TrailDispatchBufferIndices {
1881
                        dest: first_render_group_dispatch_buffer_index
1882
                            .offset(dest_group_index as u32),
1883
                        src: first_render_group_dispatch_buffer_index.offset(src_group_index),
1884
                    },
1885
                );
1886
            }
1887

1888
            let dispatch_buffer_indices = DispatchBufferIndices {
1889
                first_update_group_dispatch_buffer_index,
1890
                render_effect_metadata_buffer_index: render_effect_dispatch_buffer_id,
1891
                first_render_group_dispatch_buffer_index,
1892
                trail_dispatch_buffer_indices,
1893
            };
1894

1895
            let cache_id = effect_cache.insert(
×
1896
                added_effect.handle,
×
NEW
1897
                added_effect
×
NEW
1898
                    .groups
×
NEW
1899
                    .iter()
×
NEW
1900
                    .map(|group| group.capacity)
×
NEW
1901
                    .collect(),
×
1902
                &added_effect.particle_layout,
×
1903
                &added_effect.property_layout,
×
1904
                added_effect.layout_flags,
×
1905
                dispatch_buffer_indices,
×
NEW
1906
                added_effect.group_order,
×
1907
            );
1908

1909
            let entity = added_effect.entity;
×
1910
            self.entity_map.insert(entity, CacheEntry { cache_id });
×
1911

1912
            // Note: those effects are already in extracted_effects.effects
1913
            // because they were gathered by the same query as
1914
            // previously existing ones, during extraction.
1915

1916
            // let index = self.effect_cache.buffer_index(cache_id).unwrap();
1917
            //
1918
            // let table_id = self
1919
            // .dispatch_indirect_buffer
1920
            // .insert(GpuDispatchIndirect::default());
1921
            // assert_eq!(
1922
            // table_id.0, index,
1923
            // "Broken table invariant: buffer={} row={}",
1924
            // index, table_id.0
1925
            // );
1926
        }
1927

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

1963
const QUAD_VERTEX_POSITIONS: &[Vec3] = &[
1964
    Vec3::from_array([-0.5, -0.5, 0.0]),
1965
    Vec3::from_array([0.5, 0.5, 0.0]),
1966
    Vec3::from_array([-0.5, 0.5, 0.0]),
1967
    Vec3::from_array([-0.5, -0.5, 0.0]),
1968
    Vec3::from_array([0.5, -0.5, 0.0]),
1969
    Vec3::from_array([0.5, 0.5, 0.0]),
1970
];
1971

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

1993
impl Default for LayoutFlags {
1994
    fn default() -> Self {
1✔
1995
        Self::NONE
1✔
1996
    }
1997
}
1998

1999
pub(crate) fn prepare_effects(
10✔
2000
    mut commands: Commands,
2001
    sim_params: Res<SimParams>,
2002
    render_device: Res<RenderDevice>,
2003
    render_queue: Res<RenderQueue>,
2004
    pipeline_cache: Res<PipelineCache>,
2005
    init_pipeline: Res<ParticlesInitPipeline>,
2006
    update_pipeline: Res<ParticlesUpdatePipeline>,
2007
    mut specialized_init_pipelines: ResMut<SpecializedComputePipelines<ParticlesInitPipeline>>,
2008
    mut specialized_update_pipelines: ResMut<SpecializedComputePipelines<ParticlesUpdatePipeline>>,
2009
    // update_pipeline: Res<ParticlesUpdatePipeline>, // TODO move update_pipeline.pipeline to
2010
    // EffectsMeta
2011
    mut effects_meta: ResMut<EffectsMeta>,
2012
    mut effect_cache: ResMut<EffectCache>,
2013
    mut extracted_effects: ResMut<ExtractedEffects>,
2014
    mut effect_bind_groups: ResMut<EffectBindGroups>,
2015
) {
2016
    trace!("prepare_effects");
10✔
2017

2018
    // Allocate spawner buffer if needed
2019
    // if effects_meta.spawner_buffer.is_empty() {
2020
    //    effects_meta.spawner_buffer.push(GpuSpawnerParams::default());
2021
    //}
2022

2023
    // Write vertices (TODO - lazily once only)
2024
    effects_meta
10✔
2025
        .vertices
10✔
2026
        .write_buffer(&render_device, &render_queue);
10✔
2027

2028
    // Clear last frame's buffer resizes which may have occured during last frame,
2029
    // during `Node::run()` while the `BufferTable` could not be mutated.
2030
    effects_meta
10✔
2031
        .dispatch_indirect_buffer
10✔
2032
        .clear_previous_frame_resizes();
2033
    effects_meta
10✔
2034
        .render_effect_dispatch_buffer
10✔
2035
        .clear_previous_frame_resizes();
2036
    effects_meta
10✔
2037
        .render_group_dispatch_buffer
10✔
2038
        .clear_previous_frame_resizes();
2039

2040
    // Allocate new effects, deallocate removed ones
2041
    let removed_effect_entities = std::mem::take(&mut extracted_effects.removed_effect_entities);
10✔
2042
    for entity in &removed_effect_entities {
10✔
2043
        extracted_effects.effects.remove(entity);
×
2044
    }
2045
    effects_meta.add_remove_effects(
2046
        std::mem::take(&mut extracted_effects.added_effects),
2047
        removed_effect_entities,
2048
        &render_device,
2049
        &render_queue,
2050
        &mut effect_bind_groups,
2051
        &mut effect_cache,
2052
    );
2053

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

2063
    // Build batcher inputs from extracted effects
2064
    let effects = std::mem::take(&mut extracted_effects.effects);
2065

2066
    let effect_entity_list = effects
2067
        .into_iter()
2068
        .map(|(entity, extracted_effect)| {
×
2069
            let id = effects_meta.entity_map.get(&entity).unwrap().cache_id;
×
2070
            let property_buffer = effect_cache.get_property_buffer(id).cloned(); // clone handle for lifetime
×
2071
            let effect_slices = effect_cache.get_slices(id);
×
NEW
2072
            let group_order = effect_cache.get_group_order(id);
×
2073

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

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

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

2120
        // Create init pipeline key flags.
NEW
2121
        let mut init_pipeline_key_flags = ParticleInitPipelineKeyFlags::empty();
×
NEW
2122
        init_pipeline_key_flags.set(
×
NEW
2123
            ParticleInitPipelineKeyFlags::ATTRIBUTE_PREV,
×
NEW
2124
            input.particle_layout.contains(Attribute::PREV),
×
2125
        );
NEW
2126
        init_pipeline_key_flags.set(
×
NEW
2127
            ParticleInitPipelineKeyFlags::ATTRIBUTE_NEXT,
×
NEW
2128
            input.particle_layout.contains(Attribute::NEXT),
×
2129
        );
2130

2131
        // Specialize the init pipeline based on the effect.
NEW
2132
        let init_and_update_pipeline_ids = input
×
NEW
2133
            .effect_shaders
×
2134
            .iter()
2135
            .enumerate()
NEW
2136
            .map(|(group_index, shader)| {
×
NEW
2137
                let mut flags = init_pipeline_key_flags;
×
2138

2139
                // If this is a cloner, add the appropriate flag.
NEW
2140
                match input.initializers[group_index] {
×
NEW
2141
                    EffectInitializer::Spawner(_) => {}
×
NEW
2142
                    EffectInitializer::Cloner(_) => {
×
NEW
2143
                        flags.insert(ParticleInitPipelineKeyFlags::CLONE);
×
2144
                    }
2145
                }
2146

NEW
2147
                let init_pipeline_id = specialized_init_pipelines.specialize(
×
NEW
2148
                    &pipeline_cache,
×
NEW
2149
                    &init_pipeline,
×
NEW
2150
                    ParticleInitPipelineKey {
×
NEW
2151
                        shader: shader.init.clone(),
×
NEW
2152
                        particle_layout_min_binding_size,
×
NEW
2153
                        property_layout_min_binding_size,
×
NEW
2154
                        flags,
×
2155
                    },
2156
                );
NEW
2157
                trace!("Init pipeline specialized: id={:?}", init_pipeline_id);
×
2158

NEW
2159
                let update_pipeline_id = specialized_update_pipelines.specialize(
×
2160
                    &pipeline_cache,
2161
                    &update_pipeline,
2162
                    ParticleUpdatePipelineKey {
2163
                        shader: shader.update.clone(),
2164
                        particle_layout: input.effect_slices.particle_layout.clone(),
2165
                        property_layout: input.property_layout.clone(),
NEW
2166
                        is_trail: matches!(
×
2167
                            input.initializers[group_index],
2168
                            EffectInitializer::Cloner(_)
2169
                        ),
2170
                    },
2171
                );
NEW
2172
                trace!("Update pipeline specialized: id={:?}", update_pipeline_id);
×
2173

NEW
2174
                InitAndUpdatePipelineIds {
×
NEW
2175
                    init: init_pipeline_id,
×
NEW
2176
                    update: update_pipeline_id,
×
2177
                }
2178
            })
2179
            .collect();
2180

NEW
2181
        let init_shaders: Vec<_> = input
×
NEW
2182
            .effect_shaders
×
2183
            .iter()
NEW
2184
            .map(|shaders| shaders.init.clone())
×
2185
            .collect();
NEW
2186
        trace!("init_shader(s) = {:?}", init_shaders);
×
2187

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

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

2202
        let layout_flags = input.layout_flags;
×
2203
        trace!("layout_flags = {:?}", layout_flags);
×
2204

2205
        trace!(
×
2206
            "particle_layout = {:?}",
×
2207
            input.effect_slices.particle_layout
2208
        );
2209

2210
        #[cfg(feature = "2d")]
2211
        {
2212
            trace!("z_sort_key_2d = {:?}", input.z_sort_key_2d);
×
2213
        }
2214

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

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

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

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

2282
        let effect_cache_id = effects_meta.entity_map.get(&input.entity).unwrap().cache_id;
×
NEW
2283
        let dispatch_buffer_indices = effect_cache
×
NEW
2284
            .get_dispatch_buffer_indices(effect_cache_id)
×
2285
            .clone();
2286

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

2299
        #[cfg(feature = "2d")]
2300
        let z_sort_key_2d = input.z_sort_key_2d;
×
2301

2302
        #[cfg(feature = "3d")]
2303
        let translation_3d = input.transform.translation();
×
2304

2305
        // Spawn one shared EffectBatches for all groups of this effect. This contains
2306
        // most of the data needed to drive rendering, except the per-group data.
2307
        // However this doesn't drive rendering; this is just storage.
2308
        let batches = EffectBatches::from_input(
2309
            input,
×
2310
            spawner_base,
×
2311
            effect_cache_id,
×
NEW
2312
            init_and_update_pipeline_ids,
×
2313
            dispatch_buffer_indices,
×
2314
            first_particle_group_buffer_index.unwrap_or_default(),
×
2315
        );
2316
        let batches_entity = commands.spawn(batches).id();
×
2317

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

2333
    // Write the entire spawner buffer for this frame, for all effects combined
2334
    effects_meta
10✔
2335
        .spawner_buffer
10✔
2336
        .write_buffer(&render_device, &render_queue);
10✔
2337

2338
    // Write the entire particle group buffer for this frame
2339
    if effects_meta
10✔
2340
        .particle_group_buffer
10✔
2341
        .write_buffer(&render_device, &render_queue)
10✔
2342
    {
2343
        // The buffer changed; invalidate all bind groups for all effects.
2344
    }
2345

2346
    // Update simulation parameters
2347
    effects_meta
10✔
2348
        .sim_params_uniforms
10✔
2349
        .set(GpuSimParams::default());
10✔
2350
    {
2351
        let gpu_sim_params = effects_meta.sim_params_uniforms.get_mut();
10✔
2352
        let sim_params = *sim_params;
10✔
2353
        *gpu_sim_params = sim_params.into();
10✔
2354

2355
        gpu_sim_params.num_groups = total_group_count;
10✔
2356

2357
        trace!(
10✔
2358
            "Simulation parameters: time={} delta_time={} virtual_time={} \
×
2359
                virtual_delta_time={} real_time={} real_delta_time={} num_groups={}",
×
2360
            gpu_sim_params.time,
2361
            gpu_sim_params.delta_time,
2362
            gpu_sim_params.virtual_time,
2363
            gpu_sim_params.virtual_delta_time,
2364
            gpu_sim_params.real_time,
2365
            gpu_sim_params.real_delta_time,
2366
            gpu_sim_params.num_groups,
2367
        );
2368
    }
2369
    // FIXME - There's no simple way to tell if write_buffer() reallocates...
2370
    let prev_buffer_id = effects_meta.sim_params_uniforms.buffer().map(|b| b.id());
19✔
2371
    effects_meta
2372
        .sim_params_uniforms
2373
        .write_buffer(&render_device, &render_queue);
2374
    if prev_buffer_id != effects_meta.sim_params_uniforms.buffer().map(|b| b.id()) {
11✔
2375
        // Buffer changed, invalidate bind groups
2376
        effects_meta.sim_params_bind_group = None;
1✔
2377
    }
2378
}
2379

2380
/// The per-buffer bind group for the GPU particle buffer.
2381
pub(crate) struct BufferBindGroups {
2382
    /// Bind group for the render graphic shader.
2383
    ///
2384
    /// ```wgsl
2385
    /// @binding(0) var<storage, read> particle_buffer : ParticleBuffer;
2386
    /// @binding(1) var<storage, read> indirect_buffer : IndirectBuffer;
2387
    /// @binding(2) var<storage, read> dispatch_indirect : DispatchIndirect;
2388
    /// #ifdef RENDER_NEEDS_SPAWNER
2389
    /// @binding(3) var<storage, read> spawner : Spawner;
2390
    /// #endif
2391
    /// ```
2392
    render: BindGroup,
2393
}
2394

2395
/// Combination of a texture layout and the bound textures.
2396
#[derive(Debug, Default, Clone, PartialEq, Eq, Hash)]
2397
struct Material {
2398
    layout: TextureLayout,
2399
    textures: Vec<AssetId<Image>>,
2400
}
2401

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

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

2445
impl EffectBindGroups {
2446
    pub fn particle_render(&self, buffer_index: u32) -> Option<&BindGroup> {
×
2447
        self.particle_buffers
×
2448
            .get(&buffer_index)
×
2449
            .map(|bg| &bg.render)
×
2450
    }
2451
}
2452

2453
#[derive(SystemParam)]
2454
pub struct QueueEffectsReadOnlyParams<'w, 's> {
2455
    #[cfg(feature = "2d")]
2456
    draw_functions_2d: Res<'w, DrawFunctions<Transparent2d>>,
2457
    #[cfg(feature = "3d")]
2458
    draw_functions_3d: Res<'w, DrawFunctions<Transparent3d>>,
2459
    #[cfg(feature = "3d")]
2460
    draw_functions_alpha_mask: Res<'w, DrawFunctions<AlphaMask3d>>,
2461
    #[system_param(ignore)]
2462
    marker: PhantomData<&'s usize>,
2463
}
2464

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

2483
    for (view_entity, visible_entities, view) in views.iter() {
×
2484
        trace!("Process new sorted view");
×
2485

2486
        let Some(render_phase) = render_phases.get_mut(&view_entity) else {
×
2487
            continue;
×
2488
        };
2489

2490
        {
2491
            #[cfg(feature = "trace")]
2492
            let _span = bevy::utils::tracing::info_span!("collect_view_entities").entered();
×
2493

2494
            view_entities.clear();
×
2495
            view_entities.extend(
×
2496
                visible_entities
×
2497
                    .iter::<WithCompiledParticleEffect>()
×
2498
                    .map(|e| e.index() as usize),
×
2499
            );
2500
        }
2501

2502
        // For each view, loop over all the effect batches to determine if the effect
2503
        // needs to be rendered for that view, and enqueue a view-dependent
2504
        // batch if so.
2505
        for (draw_entity, draw_batch) in effect_draw_batches.iter() {
×
2506
            #[cfg(feature = "trace")]
2507
            let _span_draw = bevy::utils::tracing::info_span!("draw_batch").entered();
×
2508

2509
            trace!(
×
2510
                "Process draw batch: draw_entity={:?} group_index={} batches_entity={:?}",
×
2511
                draw_entity,
×
2512
                draw_batch.group_index,
×
2513
                draw_batch.batches_entity,
×
2514
            );
2515

2516
            // Get the EffectBatches this EffectDrawBatch is part of.
2517
            let Ok((batches_entity, batches)) = effect_batches.get(draw_batch.batches_entity)
×
2518
            else {
×
2519
                continue;
×
2520
            };
2521

2522
            trace!(
×
2523
                "-> EffectBaches: entity={:?} buffer_index={} spawner_base={} layout_flags={:?}",
×
2524
                batches_entity,
×
2525
                batches.buffer_index,
×
2526
                batches.spawner_base,
×
2527
                batches.layout_flags,
×
2528
            );
2529

2530
            // AlphaMask is a binned draw, so no sorted draw can possibly use it
2531
            if batches.layout_flags.contains(LayoutFlags::USE_ALPHA_MASK) {
×
2532
                continue;
×
2533
            }
2534

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

2554
            // Create and cache the bind group layout for this texture layout
2555
            render_pipeline.cache_material(&batches.texture_layout);
×
2556

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

2560
            let local_space_simulation = batches
×
2561
                .layout_flags
×
2562
                .contains(LayoutFlags::LOCAL_SPACE_SIMULATION);
×
2563
            let use_alpha_mask = batches.layout_flags.contains(LayoutFlags::USE_ALPHA_MASK);
×
2564
            let flipbook = batches.layout_flags.contains(LayoutFlags::FLIPBOOK);
×
2565
            let needs_uv = batches.layout_flags.contains(LayoutFlags::NEEDS_UV);
×
NEW
2566
            let ribbons = batches.layout_flags.contains(LayoutFlags::RIBBONS);
×
UNCOV
2567
            let image_count = batches.texture_layout.layout.len() as u8;
×
2568

2569
            // Specialize the render pipeline based on the effect batch
2570
            trace!(
×
2571
                "Specializing render pipeline: render_shaders={:?} image_count={} use_alpha_mask={:?} flipbook={:?} hdr={}",
×
2572
                batches.render_shaders,
×
2573
                image_count,
×
2574
                use_alpha_mask,
×
2575
                flipbook,
×
2576
                view.hdr
×
2577
            );
2578

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

2584
            let alpha_mode = batches.alpha_mode;
×
2585

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

2610
            trace!(
×
2611
                "+ Render pipeline specialized: id={:?} -> group_index={}",
×
2612
                render_pipeline_id,
×
2613
                draw_batch.group_index
×
2614
            );
2615
            trace!(
×
2616
                "+ Add Transparent for batch on draw_entity {:?}: buffer_index={} \
×
2617
                group_index={} spawner_base={} handle={:?}",
×
2618
                draw_entity,
×
2619
                batches.buffer_index,
×
2620
                draw_batch.group_index,
×
2621
                batches.spawner_base,
×
2622
                batches.handle
×
2623
            );
2624
            render_phase.add(make_phase_item(
×
2625
                render_pipeline_id,
×
2626
                draw_entity,
×
2627
                draw_batch,
×
2628
                draw_batch.group_index,
×
2629
                view,
×
2630
            ));
2631
        }
2632
    }
2633
}
2634

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

2655
    trace!("emit_binned_draw() {} views", views.iter().len());
×
2656

2657
    for (view_entity, visible_entities, view) in views.iter() {
×
2658
        trace!(
×
2659
            "Process new binned view (use_alpha_mask={})",
×
2660
            use_alpha_mask
×
2661
        );
2662

2663
        let Some(render_phase) = render_phases.get_mut(&view_entity) else {
×
2664
            continue;
×
2665
        };
2666

2667
        {
2668
            #[cfg(feature = "trace")]
2669
            let _span = bevy::utils::tracing::info_span!("collect_view_entities").entered();
×
2670

2671
            view_entities.clear();
×
2672
            view_entities.extend(
×
2673
                visible_entities
×
2674
                    .iter::<WithCompiledParticleEffect>()
×
2675
                    .map(|e| e.index() as usize),
×
2676
            );
2677
        }
2678

2679
        // For each view, loop over all the effect batches to determine if the effect
2680
        // needs to be rendered for that view, and enqueue a view-dependent
2681
        // batch if so.
2682
        for (draw_entity, draw_batch) in effect_draw_batches.iter() {
×
2683
            #[cfg(feature = "trace")]
2684
            let _span_draw = bevy::utils::tracing::info_span!("draw_batch").entered();
×
2685

2686
            trace!(
×
2687
                "Process draw batch: draw_entity={:?} group_index={} batches_entity={:?}",
×
2688
                draw_entity,
×
2689
                draw_batch.group_index,
×
2690
                draw_batch.batches_entity,
×
2691
            );
2692

2693
            // Get the EffectBatches this EffectDrawBatch is part of.
2694
            let Ok((batches_entity, batches)) = effect_batches.get(draw_batch.batches_entity)
×
2695
            else {
×
2696
                continue;
×
2697
            };
2698

2699
            trace!(
×
2700
                "-> EffectBaches: entity={:?} buffer_index={} spawner_base={} layout_flags={:?}",
×
2701
                batches_entity,
×
2702
                batches.buffer_index,
×
2703
                batches.spawner_base,
×
2704
                batches.layout_flags,
×
2705
            );
2706

2707
            if use_alpha_mask != batches.layout_flags.contains(LayoutFlags::USE_ALPHA_MASK) {
×
2708
                continue;
×
2709
            }
2710

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

2730
            // Create and cache the bind group layout for this texture layout
2731
            render_pipeline.cache_material(&batches.texture_layout);
×
2732

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

2736
            let local_space_simulation = batches
×
2737
                .layout_flags
×
2738
                .contains(LayoutFlags::LOCAL_SPACE_SIMULATION);
×
2739
            let use_alpha_mask = batches.layout_flags.contains(LayoutFlags::USE_ALPHA_MASK);
×
2740
            let flipbook = batches.layout_flags.contains(LayoutFlags::FLIPBOOK);
×
2741
            let needs_uv = batches.layout_flags.contains(LayoutFlags::NEEDS_UV);
×
NEW
2742
            let ribbons = batches.layout_flags.contains(LayoutFlags::RIBBONS);
×
UNCOV
2743
            let image_count = batches.texture_layout.layout.len() as u8;
×
2744

2745
            // Specialize the render pipeline based on the effect batch
2746
            trace!(
×
2747
                "Specializing render pipeline: render_shaders={:?} image_count={} use_alpha_mask={:?} flipbook={:?} hdr={}",
×
2748
                batches.render_shaders,
×
2749
                image_count,
×
2750
                use_alpha_mask,
×
2751
                flipbook,
×
2752
                view.hdr
×
2753
            );
2754

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

2760
            let alpha_mode = batches.alpha_mode;
×
2761

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

2786
            trace!(
×
2787
                "+ Render pipeline specialized: id={:?} -> group_index={}",
×
2788
                render_pipeline_id,
×
2789
                draw_batch.group_index
×
2790
            );
2791
            trace!(
×
2792
                "+ Add Transparent for batch on draw_entity {:?}: buffer_index={} \
×
2793
                group_index={} spawner_base={} handle={:?}",
×
2794
                draw_entity,
×
2795
                batches.buffer_index,
×
2796
                draw_batch.group_index,
×
2797
                batches.spawner_base,
×
2798
                batches.handle
×
2799
            );
2800
            render_phase.add(
×
2801
                make_bin_key(render_pipeline_id, draw_batch, draw_batch.group_index, view),
×
2802
                draw_entity,
×
2803
                BinnedRenderPhaseType::NonMesh,
×
2804
            );
2805
        }
2806
    }
2807
}
2808

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

2836
    trace!("queue_effects");
×
2837

2838
    // If an image has changed, the GpuImage has (probably) changed
2839
    for event in &events.images {
18✔
2840
        match event {
8✔
2841
            AssetEvent::Added { .. } => None,
5✔
2842
            AssetEvent::LoadedWithDependencies { .. } => None,
×
2843
            AssetEvent::Unused { .. } => None,
×
2844
            AssetEvent::Modified { id } => {
×
2845
                trace!("Destroy bind group of modified image asset {:?}", id);
×
2846
                effect_bind_groups.images.remove(id)
×
2847
            }
2848
            AssetEvent::Removed { id } => {
3✔
2849
                trace!("Destroy bind group of removed image asset {:?}", id);
3✔
2850
                effect_bind_groups.images.remove(id)
3✔
2851
            }
2852
        };
2853
    }
2854

2855
    if effects_meta.spawner_buffer.buffer().is_none() || effects_meta.spawner_buffer.is_empty() {
10✔
2856
        // No spawners are active
2857
        return;
10✔
2858
    }
2859

2860
    // Loop over all 2D cameras/views that need to render effects
2861
    #[cfg(feature = "2d")]
2862
    {
2863
        #[cfg(feature = "trace")]
2864
        let _span_draw = bevy::utils::tracing::info_span!("draw_2d").entered();
×
2865

2866
        let draw_effects_function_2d = read_params
2867
            .draw_functions_2d
2868
            .read()
2869
            .get_id::<DrawEffects>()
2870
            .unwrap();
2871

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

2899
    // Loop over all 3D cameras/views that need to render effects
2900
    #[cfg(feature = "3d")]
2901
    {
2902
        #[cfg(feature = "trace")]
2903
        let _span_draw = bevy::utils::tracing::info_span!("draw_3d").entered();
×
2904

2905
        // Effects with full alpha blending
2906
        if !views.is_empty() {
2907
            trace!("Emit effect draw calls for alpha blended 3D views...");
×
2908

2909
            let draw_effects_function_3d = read_params
×
2910
                .draw_functions_3d
×
2911
                .read()
2912
                .get_id::<DrawEffects>()
2913
                .unwrap();
2914

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

2940
        // Effects with alpha mask
2941
        if !views.is_empty() {
×
2942
            #[cfg(feature = "trace")]
2943
            let _span_draw = bevy::utils::tracing::info_span!("draw_alphamask").entered();
×
2944

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

2947
            let draw_effects_function_alpha_mask = read_params
×
2948
                .draw_functions_alpha_mask
×
2949
                .read()
2950
                .get_id::<DrawEffects>()
2951
                .unwrap();
2952

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

2983
/// Prepare GPU resources for effect rendering.
2984
///
2985
/// This system runs in the [`Prepare`] render set, after Bevy has updated the
2986
/// [`ViewUniforms`], which need to be referenced to get access to the current
2987
/// camera view.
2988
pub(crate) fn prepare_resources(
10✔
2989
    mut effects_meta: ResMut<EffectsMeta>,
2990
    render_device: Res<RenderDevice>,
2991
    view_uniforms: Res<ViewUniforms>,
2992
    render_pipeline: Res<ParticlesRenderPipeline>,
2993
) {
2994
    // Get the binding for the ViewUniform, the uniform data structure containing
2995
    // the Camera data for the current view.
2996
    let Some(view_binding) = view_uniforms.uniforms.binding() else {
20✔
2997
        return;
×
2998
    };
2999

3000
    // Create the bind group for the camera/view parameters
3001
    effects_meta.view_bind_group = Some(render_device.create_bind_group(
3002
        "hanabi:bind_group_camera_view",
3003
        &render_pipeline.view_layout,
3004
        &[
3005
            BindGroupEntry {
3006
                binding: 0,
3007
                resource: view_binding,
3008
            },
3009
            BindGroupEntry {
3010
                binding: 1,
3011
                resource: effects_meta.sim_params_uniforms.binding().unwrap(),
3012
            },
3013
        ],
3014
    ));
3015
}
3016

3017
pub(crate) fn prepare_bind_groups(
10✔
3018
    mut effects_meta: ResMut<EffectsMeta>,
3019
    mut effect_cache: ResMut<EffectCache>,
3020
    mut effect_bind_groups: ResMut<EffectBindGroups>,
3021
    effect_batches: Query<(Entity, &mut EffectBatches)>,
3022
    render_device: Res<RenderDevice>,
3023
    dispatch_indirect_pipeline: Res<DispatchIndirectPipeline>,
3024
    init_pipeline: Res<ParticlesInitPipeline>,
3025
    update_pipeline: Res<ParticlesUpdatePipeline>,
3026
    render_pipeline: ResMut<ParticlesRenderPipeline>,
3027
    gpu_images: Res<RenderAssets<GpuImage>>,
3028
) {
3029
    if effects_meta.spawner_buffer.is_empty() || effects_meta.spawner_buffer.buffer().is_none() {
10✔
3030
        return;
10✔
3031
    }
3032

3033
    {
3034
        #[cfg(feature = "trace")]
3035
        let _span = bevy::utils::tracing::info_span!("shared_bind_groups").entered();
×
3036

3037
        // Create the bind group for the global simulation parameters
3038
        if effects_meta.sim_params_bind_group.is_none() {
×
3039
            effects_meta.sim_params_bind_group = Some(render_device.create_bind_group(
×
3040
                "hanabi:bind_group_sim_params",
×
3041
                &update_pipeline.sim_params_layout, /* FIXME - Shared with vfx_update, is
×
3042
                                                     * that OK? */
×
3043
                &[BindGroupEntry {
×
3044
                    binding: 0,
×
3045
                    resource: effects_meta.sim_params_uniforms.binding().unwrap(),
×
3046
                }],
3047
            ));
3048
        }
3049

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

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

NEW
3148
        let init_render_indirect_bind_group = match (
×
3149
            effects_meta.render_effect_dispatch_buffer.buffer(),
3150
            effects_meta.render_group_dispatch_buffer.buffer(),
3151
        ) {
NEW
3152
            (Some(render_effect_dispatch_buffer), Some(render_group_dispatch_buffer)) => {
×
NEW
3153
                Some(render_device.create_bind_group(
×
NEW
3154
                    "hanabi:bind_group_init_render_dispatch",
×
NEW
3155
                    &init_pipeline.render_indirect_layout,
×
NEW
3156
                    &[
×
NEW
3157
                        BindGroupEntry {
×
NEW
3158
                            binding: 0,
×
NEW
3159
                            resource: BindingResource::Buffer(BufferBinding {
×
NEW
3160
                                buffer: render_effect_dispatch_buffer,
×
NEW
3161
                                offset: 0,
×
NEW
3162
                                size: Some(effects_meta.gpu_limits.render_effect_indirect_size()),
×
3163
                            }),
3164
                        },
NEW
3165
                        BindGroupEntry {
×
NEW
3166
                            binding: 1,
×
NEW
3167
                            resource: BindingResource::Buffer(BufferBinding {
×
NEW
3168
                                buffer: render_group_dispatch_buffer,
×
NEW
3169
                                offset: 0,
×
NEW
3170
                                size: Some(effects_meta.gpu_limits.render_group_indirect_size()),
×
3171
                            }),
3172
                        },
NEW
3173
                        BindGroupEntry {
×
NEW
3174
                            binding: 2,
×
NEW
3175
                            resource: BindingResource::Buffer(BufferBinding {
×
NEW
3176
                                buffer: render_group_dispatch_buffer,
×
NEW
3177
                                offset: 0,
×
NEW
3178
                                size: Some(effects_meta.gpu_limits.render_group_indirect_size()),
×
3179
                            }),
3180
                        },
3181
                    ],
3182
                ))
3183
            }
3184

NEW
3185
            (_, _) => None,
×
3186
        };
3187

3188
        // Create the bind group for the indirect render buffer use in the init shader
3189
        effects_meta.init_render_indirect_bind_group = init_render_indirect_bind_group;
3190
    }
3191

3192
    // Make a copy of the buffer ID before borrowing effects_meta mutably in the
3193
    // loop below
NEW
3194
    let Some(indirect_buffer) = effects_meta.dispatch_indirect_buffer.buffer().cloned() else {
×
NEW
3195
        return;
×
3196
    };
NEW
3197
    let Some(spawner_buffer) = effects_meta.spawner_buffer.buffer().cloned() else {
×
NEW
3198
        return;
×
3199
    };
3200

3201
    // Create the per-effect render bind groups
3202
    trace!("Create per-effect render bind groups...");
×
3203
    for (buffer_index, buffer) in effect_cache.buffers().iter().enumerate() {
×
3204
        #[cfg(feature = "trace")]
3205
        let _span_buffer = bevy::utils::tracing::info_span!("create_buffer_bind_groups").entered();
×
3206

3207
        let Some(buffer) = buffer else {
×
3208
            trace!(
×
3209
                "Effect buffer index #{} has no allocated EffectBuffer, skipped.",
×
3210
                buffer_index
3211
            );
3212
            continue;
×
3213
        };
3214

3215
        // Ensure all effect groups have a bind group for the entire buffer of the
3216
        // group, since the update phase runs on an entire group/buffer at once,
3217
        // with all the effect instances in it batched together.
3218
        trace!("effect particle buffer_index=#{}", buffer_index);
×
3219
        effect_bind_groups
×
3220
            .particle_buffers
×
3221
            .entry(buffer_index as u32)
×
3222
            .or_insert_with(|| {
×
3223
                trace!(
×
3224
                    "Create new particle bind groups for buffer_index={} | particle_layout {:?} | property_layout {:?}",
×
3225
                    buffer_index,
×
3226
                    buffer.particle_layout(),
×
3227
                    buffer.property_layout(),
×
3228
                );
3229

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

3268
                BufferBindGroups {
×
3269
                    render,
×
3270
                }
3271
            });
3272
    }
3273

3274
    // Create the per-effect bind groups.
3275
    for (entity, effect_batches) in effect_batches.iter() {
×
3276
        #[cfg(feature = "trace")]
3277
        let _span_buffer = bevy::utils::tracing::info_span!("create_batch_bind_groups").entered();
×
3278

3279
        let effect_cache_id = effect_batches.effect_cache_id;
3280

3281
        // Convert indirect buffer offsets from indices to bytes.
3282
        let first_effect_particle_group_buffer_offset = effects_meta
3283
            .gpu_limits
3284
            .particle_group_offset(effect_batches.first_particle_group_buffer_index)
3285
            as u64;
3286
        let effect_particle_groups_buffer_size = NonZeroU64::try_from(
3287
            u32::from(effects_meta.gpu_limits.particle_group_aligned_size) as u64
3288
                * effect_batches.group_batches.len() as u64,
3289
        )
3290
        .unwrap();
3291
        let group_binding = BufferBinding {
3292
            buffer: effects_meta.particle_group_buffer.buffer().unwrap(),
3293
            offset: first_effect_particle_group_buffer_offset,
3294
            size: Some(effect_particle_groups_buffer_size),
3295
        };
3296

3297
        let Some(Some(effect_buffer)) = effect_cache
×
3298
            .buffers_mut()
3299
            .get_mut(effect_batches.buffer_index as usize)
3300
        else {
3301
            error!("No particle buffer allocated for entity {:?}", entity);
×
3302
            continue;
×
3303
        };
3304

3305
        // Bind group for the init compute shader to simulate particles.
3306
        // TODO - move this creation in RenderSet::PrepareBindGroups
3307
        effect_buffer.create_sim_bind_group(
×
3308
            effect_batches.buffer_index,
×
3309
            &render_device,
×
3310
            group_binding,
×
3311
        );
3312

3313
        if effect_bind_groups
×
3314
            .update_render_indirect_bind_groups
×
3315
            .get(&effect_cache_id)
×
3316
            .is_none()
3317
        {
3318
            let DispatchBufferIndices {
×
3319
                render_effect_metadata_buffer_index: render_effect_dispatch_buffer_index,
×
3320
                first_render_group_dispatch_buffer_index,
×
3321
                ..
×
3322
            } = effect_batches.dispatch_buffer_indices;
×
3323

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

3359
            effect_bind_groups
×
3360
                .update_render_indirect_bind_groups
×
3361
                .insert(
3362
                    effect_cache_id,
×
3363
                    particles_buffer_layout_update_render_indirect,
×
3364
                );
3365
        }
3366

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

3382
            // TODO = move
3383
            let material = Material {
3384
                layout: effect_batches.texture_layout.clone(),
3385
                textures: effect_batches.textures.iter().map(|h| h.id()).collect(),
×
3386
            };
3387
            assert_eq!(material.layout.layout.len(), material.textures.len());
3388

3389
            let bind_group_entries = material.make_entries(&gpu_images);
×
3390

3391
            effect_bind_groups
×
3392
                .material_bind_groups
×
3393
                .entry(material.clone())
×
3394
                .or_insert_with(|| {
×
3395
                    render_device.create_bind_group(
×
3396
                        &format!(
×
3397
                            "hanabi:material_bind_group_{}",
×
3398
                            material.layout.layout.len()
×
3399
                        )[..],
×
3400
                        material_bind_group_layout,
×
3401
                        &bind_group_entries[..],
×
3402
                    )
3403
                });
3404
        }
3405
    }
3406
}
3407

3408
type DrawEffectsSystemState = SystemState<(
3409
    SRes<EffectsMeta>,
3410
    SRes<EffectBindGroups>,
3411
    SRes<PipelineCache>,
3412
    SQuery<Read<ViewUniformOffset>>,
3413
    SQuery<Read<EffectBatches>>,
3414
    SQuery<Read<EffectDrawBatch>>,
3415
)>;
3416

3417
/// Draw function for rendering all active effects for the current frame.
3418
///
3419
/// Effects are rendered in the [`Transparent2d`] phase of the main 2D pass,
3420
/// and the [`Transparent3d`] phase of the main 3D pass.
3421
pub(crate) struct DrawEffects {
3422
    params: DrawEffectsSystemState,
3423
}
3424

3425
impl DrawEffects {
3426
    pub fn new(world: &mut World) -> Self {
3✔
3427
        Self {
3428
            params: SystemState::new(world),
3✔
3429
        }
3430
    }
3431
}
3432

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

3452
    let gpu_limits = &effects_meta.gpu_limits;
×
3453

3454
    let Some(pipeline) = pipeline_cache.into_inner().get_render_pipeline(pipeline_id) else {
×
3455
        return;
×
3456
    };
3457

3458
    trace!("render pass");
×
3459

3460
    pass.set_render_pipeline(pipeline);
×
3461

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

3465
    // View properties (camera matrix, etc.)
3466
    pass.set_bind_group(
×
3467
        0,
3468
        effects_meta.view_bind_group.as_ref().unwrap(),
×
3469
        &[view_uniform.offset],
×
3470
    );
3471

3472
    // Particles buffer
3473
    let dispatch_indirect_offset = gpu_limits.dispatch_indirect_offset(effect_batches.buffer_index);
×
3474
    trace!(
×
3475
        "set_bind_group(1): dispatch_indirect_offset={}",
×
3476
        dispatch_indirect_offset
×
3477
    );
3478
    let spawner_base = effect_batches.spawner_base;
×
3479
    let spawner_buffer_aligned = effects_meta.spawner_buffer.aligned_size();
×
3480
    assert!(spawner_buffer_aligned >= GpuSpawnerParams::min_size().get() as usize);
×
3481
    let spawner_offset = spawner_base * spawner_buffer_aligned as u32;
×
3482
    let dyn_uniform_indices: [u32; 2] = [dispatch_indirect_offset, spawner_offset];
×
3483
    let dyn_uniform_indices = if effect_batches
×
3484
        .layout_flags
×
3485
        .contains(LayoutFlags::LOCAL_SPACE_SIMULATION)
×
3486
    {
3487
        &dyn_uniform_indices
×
3488
    } else {
3489
        &dyn_uniform_indices[..1]
×
3490
    };
3491
    pass.set_bind_group(
×
3492
        1,
3493
        effect_bind_groups
×
3494
            .particle_render(effect_batches.buffer_index)
×
3495
            .unwrap(),
×
3496
        dyn_uniform_indices,
×
3497
    );
3498

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

3518
    let render_indirect_buffer = effects_meta.render_group_dispatch_buffer.buffer().unwrap();
×
3519
    let group_index = effect_draw_batch.group_index;
×
3520
    let effect_batch = &effect_batches.group_batches[group_index as usize];
×
3521

3522
    let render_group_dispatch_indirect_index = effect_batches
×
3523
        .dispatch_buffer_indices
×
3524
        .first_render_group_dispatch_buffer_index
×
3525
        .0
×
3526
        + group_index;
×
3527

3528
    trace!(
×
3529
        "Draw up to {} particles with {} vertices per particle for batch from buffer #{} \
×
3530
            (render_group_dispatch_indirect_index={:?}, group_index={}).",
×
3531
        effect_batch.slice.len(),
×
3532
        effects_meta.vertices.len(),
×
3533
        effect_batches.buffer_index,
×
3534
        render_group_dispatch_indirect_index,
×
3535
        group_index,
×
3536
    );
3537

3538
    pass.draw_indirect(
×
3539
        render_indirect_buffer,
×
3540
        render_group_dispatch_indirect_index as u64
×
3541
            * u32::from(gpu_limits.render_group_indirect_aligned_size) as u64,
×
3542
    );
3543
}
3544

3545
#[cfg(feature = "2d")]
3546
impl Draw<Transparent2d> for DrawEffects {
3547
    fn draw<'w>(
×
3548
        &mut self,
3549
        world: &'w World,
3550
        pass: &mut TrackedRenderPass<'w>,
3551
        view: Entity,
3552
        item: &Transparent2d,
3553
    ) {
3554
        trace!("Draw<Transparent2d>: view={:?}", view);
×
3555
        draw(
3556
            world,
×
3557
            pass,
×
3558
            view,
×
3559
            item.entity,
×
3560
            item.pipeline,
×
3561
            &mut self.params,
×
3562
        );
3563
    }
3564
}
3565

3566
#[cfg(feature = "3d")]
3567
impl Draw<Transparent3d> for DrawEffects {
3568
    fn draw<'w>(
×
3569
        &mut self,
3570
        world: &'w World,
3571
        pass: &mut TrackedRenderPass<'w>,
3572
        view: Entity,
3573
        item: &Transparent3d,
3574
    ) {
3575
        trace!("Draw<Transparent3d>: view={:?}", view);
×
3576
        draw(
3577
            world,
×
3578
            pass,
×
3579
            view,
×
3580
            item.entity,
×
3581
            item.pipeline,
×
3582
            &mut self.params,
×
3583
        );
3584
    }
3585
}
3586

3587
#[cfg(feature = "3d")]
3588
impl Draw<AlphaMask3d> for DrawEffects {
3589
    fn draw<'w>(
×
3590
        &mut self,
3591
        world: &'w World,
3592
        pass: &mut TrackedRenderPass<'w>,
3593
        view: Entity,
3594
        item: &AlphaMask3d,
3595
    ) {
3596
        trace!("Draw<AlphaMask3d>: view={:?}", view);
×
3597
        draw(
3598
            world,
×
3599
            pass,
×
3600
            view,
×
3601
            item.representative_entity,
×
3602
            item.key.pipeline,
×
3603
            &mut self.params,
×
3604
        );
3605
    }
3606
}
3607

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

NEW
3664
    trace!(
×
NEW
3665
        "Creating particle bind group layout '{}' for init pass with {} entries.",
×
NEW
3666
        label,
×
NEW
3667
        entries.len()
×
3668
    );
NEW
3669
    render_device.create_bind_group_layout(label, &entries)
×
3670
}
3671

3672
fn create_init_render_indirect_bind_group_layout(
1✔
3673
    render_device: &RenderDevice,
3674
    label: &str,
3675
    clone: bool,
3676
) -> BindGroupLayout {
3677
    let storage_alignment = render_device.limits().min_storage_buffer_offset_alignment;
1✔
3678
    let render_effect_indirect_size = GpuRenderEffectMetadata::aligned_size(storage_alignment);
1✔
3679
    let render_group_indirect_size = GpuRenderGroupIndirect::aligned_size(storage_alignment);
1✔
3680

3681
    let mut entries = vec![
1✔
3682
        // @binding(0) var<storage, read_write> render_effect_indirect :
3683
        // RenderEffectMetadata
3684
        BindGroupLayoutEntry {
1✔
3685
            binding: 0,
1✔
3686
            visibility: ShaderStages::COMPUTE,
1✔
3687
            ty: BindingType::Buffer {
1✔
3688
                ty: BufferBindingType::Storage { read_only: false },
1✔
3689
                has_dynamic_offset: true,
1✔
3690
                min_binding_size: Some(render_effect_indirect_size),
1✔
3691
            },
3692
            count: None,
1✔
3693
        },
3694
        // @binding(1) var<storage, read_write> dest_render_group_indirect : RenderGroupIndirect
3695
        BindGroupLayoutEntry {
1✔
3696
            binding: 1,
1✔
3697
            visibility: ShaderStages::COMPUTE,
1✔
3698
            ty: BindingType::Buffer {
1✔
3699
                ty: BufferBindingType::Storage { read_only: false },
1✔
3700
                has_dynamic_offset: true,
1✔
3701
                min_binding_size: Some(render_group_indirect_size),
1✔
3702
            },
3703
            count: None,
1✔
3704
        },
3705
    ];
3706

3707
    if clone {
2✔
3708
        // @binding(2) var<storage, read_write> src_render_group_indirect : RenderGroupIndirect
3709
        entries.push(BindGroupLayoutEntry {
1✔
3710
            binding: 2,
1✔
3711
            visibility: ShaderStages::COMPUTE,
1✔
3712
            ty: BindingType::Buffer {
1✔
3713
                ty: BufferBindingType::Storage { read_only: false },
1✔
3714
                has_dynamic_offset: true,
1✔
3715
                min_binding_size: Some(render_group_indirect_size),
1✔
3716
            },
3717
            count: None,
1✔
3718
        });
3719
    }
3720

3721
    render_device.create_bind_group_layout(label, &entries)
1✔
3722
}
3723

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

NEW
3781
    trace!(
×
NEW
3782
        "Creating particle bind group layout '{}' for update pass with {} entries.",
×
NEW
3783
        label,
×
NEW
3784
        entries.len()
×
3785
    );
NEW
3786
    render_device.create_bind_group_layout(label, &entries)
×
3787
}
3788

3789
/// Render node to run the simulation sub-graph once per frame.
3790
///
3791
/// This node doesn't simulate anything by itself, but instead schedules the
3792
/// simulation sub-graph, where other nodes like [`VfxSimulateNode`] do the
3793
/// actual simulation.
3794
///
3795
/// The simulation sub-graph is scheduled to run before the [`CameraDriverNode`]
3796
/// renders all the views, such that rendered views have access to the
3797
/// just-simulated particles to render them.
3798
///
3799
/// [`CameraDriverNode`]: bevy::render::camera::CameraDriverNode
3800
pub(crate) struct VfxSimulateDriverNode;
3801

3802
impl Node for VfxSimulateDriverNode {
3803
    fn run(
10✔
3804
        &self,
3805
        graph: &mut RenderGraphContext,
3806
        _render_context: &mut RenderContext,
3807
        _world: &World,
3808
    ) -> Result<(), NodeRunError> {
3809
        graph.run_sub_graph(
10✔
3810
            crate::plugin::simulate_graph::HanabiSimulateGraph,
10✔
3811
            vec![],
10✔
3812
            None,
10✔
3813
        )?;
3814
        Ok(())
10✔
3815
    }
3816
}
3817

3818
/// Render node to run the simulation of all effects once per frame.
3819
///
3820
/// Runs inside the simulation sub-graph, looping over all extracted effect
3821
/// batches to simulate them.
3822
pub(crate) struct VfxSimulateNode {
3823
    /// Query to retrieve the batches of effects to simulate and render.
3824
    effect_query: QueryState<(Entity, Read<EffectBatches>)>,
3825
}
3826

3827
impl VfxSimulateNode {
3828
    /// Output particle buffer for that view. TODO - how to handle multiple
3829
    /// buffers?! Should use Entity instead??
3830
    // pub const OUT_PARTICLE_BUFFER: &'static str = "particle_buffer";
3831

3832
    /// Create a new node for simulating the effects of the given world.
3833
    pub fn new(world: &mut World) -> Self {
1✔
3834
        Self {
3835
            effect_query: QueryState::new(world),
1✔
3836
        }
3837
    }
3838
}
3839

3840
impl Node for VfxSimulateNode {
3841
    fn input(&self) -> Vec<SlotInfo> {
1✔
3842
        vec![]
1✔
3843
    }
3844

3845
    fn update(&mut self, world: &mut World) {
10✔
3846
        trace!("VfxSimulateNode::update()");
10✔
3847
        self.effect_query.update_archetypes(world);
10✔
3848
    }
3849

3850
    fn run(
10✔
3851
        &self,
3852
        _graph: &mut RenderGraphContext,
3853
        render_context: &mut RenderContext,
3854
        world: &World,
3855
    ) -> Result<(), NodeRunError> {
3856
        trace!("VfxSimulateNode::run()");
10✔
3857

3858
        // Get the Entity containing the ViewEffectsEntity component used as container
3859
        // for the input data for this node.
3860
        // let view_entity = graph.get_input_entity(Self::IN_VIEW)?;
3861
        let pipeline_cache = world.resource::<PipelineCache>();
10✔
3862

3863
        let effects_meta = world.resource::<EffectsMeta>();
10✔
3864
        let effect_cache = world.resource::<EffectCache>();
10✔
3865
        let effect_bind_groups = world.resource::<EffectBindGroups>();
10✔
3866
        let dispatch_indirect_pipeline = world.resource::<DispatchIndirectPipeline>();
10✔
3867
        // let render_queue = world.resource::<RenderQueue>();
3868

3869
        // Make sure to schedule any buffer copy from changed effects before accessing
3870
        // them
3871
        {
3872
            let command_encoder = render_context.command_encoder();
10✔
3873
            effects_meta
10✔
3874
                .dispatch_indirect_buffer
10✔
3875
                .write_buffer(command_encoder);
10✔
3876
            effects_meta
10✔
3877
                .render_effect_dispatch_buffer
10✔
3878
                .write_buffer(command_encoder);
10✔
3879
            effects_meta
10✔
3880
                .render_group_dispatch_buffer
10✔
3881
                .write_buffer(command_encoder);
10✔
3882
        }
3883

3884
        // Compute init pass
3885
        // let mut total_group_count = 0;
3886
        {
3887
            {
3888
                trace!("loop over effect batches...");
10✔
3889

3890
                // Dispatch init compute jobs
3891
                for (entity, batches) in self.effect_query.iter_manual(world) {
10✔
NEW
3892
                    for &dest_group_index in batches.group_order.iter() {
×
NEW
3893
                        let initializer = &batches.initializers[dest_group_index as usize];
×
3894
                        let dest_render_group_dispatch_buffer_index = BufferTableId(
NEW
3895
                            batches
×
NEW
3896
                                .dispatch_buffer_indices
×
NEW
3897
                                .first_render_group_dispatch_buffer_index
×
NEW
3898
                                .0
×
NEW
3899
                                + dest_group_index,
×
3900
                        );
3901

3902
                        // Destination group spawners are packed one after one another.
NEW
3903
                        let spawner_base = batches.spawner_base + dest_group_index;
×
NEW
3904
                        let spawner_buffer_aligned = effects_meta.spawner_buffer.aligned_size();
×
NEW
3905
                        assert!(
×
NEW
3906
                            spawner_buffer_aligned >= GpuSpawnerParams::min_size().get() as usize
×
3907
                        );
NEW
3908
                        let spawner_offset = spawner_base * spawner_buffer_aligned as u32;
×
3909

NEW
3910
                        match initializer {
×
NEW
3911
                            EffectInitializer::Spawner(effect_spawner) => {
×
NEW
3912
                                let mut compute_pass = render_context
×
3913
                                    .command_encoder()
NEW
3914
                                    .begin_compute_pass(&ComputePassDescriptor {
×
NEW
3915
                                        label: Some("hanabi:init"),
×
NEW
3916
                                        timestamp_writes: None,
×
3917
                                    });
3918

NEW
3919
                                let render_effect_dispatch_buffer_index = batches
×
NEW
3920
                                    .dispatch_buffer_indices
×
NEW
3921
                                    .render_effect_metadata_buffer_index;
×
3922

3923
                                // FIXME - Currently we unconditionally count
3924
                                // all groups because the dispatch pass always
3925
                                // runs on all groups. We should consider if
3926
                                // it's worth skipping e.g. dormant or finished
3927
                                // effects at the cost of extra complexity.
3928
                                // total_group_count += batches.group_batches.len() as u32;
3929

NEW
3930
                                let Some(init_pipeline) = pipeline_cache.get_compute_pipeline(
×
3931
                                    batches.init_and_update_pipeline_ids[dest_group_index as usize]
3932
                                        .init,
3933
                                ) else {
NEW
3934
                                    if let CachedPipelineState::Err(err) = pipeline_cache
×
3935
                                        .get_compute_pipeline_state(
NEW
3936
                                            batches.init_and_update_pipeline_ids
×
NEW
3937
                                                [dest_group_index as usize]
×
NEW
3938
                                                .init,
×
3939
                                        )
3940
                                    {
3941
                                        error!(
NEW
3942
                                            "Failed to find init pipeline #{} for effect {:?}: \
×
NEW
3943
                                             {:?}",
×
NEW
3944
                                            batches.init_and_update_pipeline_ids
×
NEW
3945
                                                [dest_group_index as usize]
×
NEW
3946
                                                .init
×
NEW
3947
                                                .id(),
×
3948
                                            entity,
3949
                                            err
3950
                                        );
3951
                                    }
NEW
3952
                                    continue;
×
3953
                                };
3954

3955
                                // Do not dispatch any init work if there's nothing to spawn this frame
3956
                                let spawn_count = effect_spawner.spawn_count;
3957
                                if spawn_count == 0 {
NEW
3958
                                    continue;
×
3959
                                }
3960

3961
                                const WORKGROUP_SIZE: u32 = 64;
3962
                                let workgroup_count =
3963
                                    (spawn_count + WORKGROUP_SIZE - 1) / WORKGROUP_SIZE;
3964

3965
                                let effect_cache_id = batches.effect_cache_id;
3966

3967
                                // for (effect_entity, effect_slice) in effects_meta.entity_map.iter()
3968
                                // Retrieve the ExtractedEffect from the entity
3969
                                // trace!("effect_entity={:?} effect_slice={:?}", effect_entity,
3970
                                // effect_slice); let effect =
3971
                                // self.effect_query.get_manual(world, *effect_entity).unwrap();
3972

3973
                                // Get the slice to init
3974
                                // let effect_slice = effects_meta.get(&effect_entity);
3975
                                // let effect_group =
3976
                                //     &effects_meta.effect_cache.buffers()[batch.buffer_index as usize];
NEW
3977
                                let Some(particles_init_bind_group) =
×
3978
                                    effect_cache.init_bind_group(effect_cache_id)
3979
                                else {
NEW
3980
                                    error!(
×
NEW
3981
                                        "Failed to find init particle buffer bind group for \
×
NEW
3982
                                         entity {:?}",
×
3983
                                        entity
3984
                                    );
NEW
3985
                                    continue;
×
3986
                                };
3987

3988
                                let render_effect_indirect_offset =
3989
                                    effects_meta.gpu_limits.render_effect_indirect_offset(
3990
                                        render_effect_dispatch_buffer_index.0,
3991
                                    );
3992

3993
                                let render_group_indirect_offset =
3994
                                    effects_meta.gpu_limits.render_group_indirect_offset(
3995
                                        dest_render_group_dispatch_buffer_index.0,
3996
                                    );
3997

3998
                                trace!(
NEW
3999
                                    "record commands for init pipeline of effect {:?} \
×
NEW
4000
                                        (spawn {} = {} workgroups) spawner_base={} \
×
NEW
4001
                                        spawner_offset={} \
×
NEW
4002
                                        render_effect_indirect_offset={} \
×
NEW
4003
                                        first_render_group_indirect_offset={}...",
×
4004
                                    batches.handle,
4005
                                    spawn_count,
4006
                                    workgroup_count,
4007
                                    spawner_base,
4008
                                    spawner_offset,
4009
                                    render_effect_indirect_offset,
4010
                                    render_group_indirect_offset,
4011
                                );
4012

4013
                                // Setup compute pass
NEW
4014
                                compute_pass.set_pipeline(init_pipeline);
×
NEW
4015
                                compute_pass.set_bind_group(
×
4016
                                    0,
NEW
4017
                                    effects_meta.sim_params_bind_group.as_ref().unwrap(),
×
NEW
4018
                                    &[],
×
4019
                                );
NEW
4020
                                compute_pass.set_bind_group(1, particles_init_bind_group, &[]);
×
NEW
4021
                                compute_pass.set_bind_group(
×
4022
                                    2,
NEW
4023
                                    effects_meta.spawner_bind_group.as_ref().unwrap(),
×
NEW
4024
                                    &[spawner_offset],
×
4025
                                );
NEW
4026
                                compute_pass.set_bind_group(
×
4027
                                    3,
NEW
4028
                                    effects_meta
×
NEW
4029
                                        .init_render_indirect_bind_group
×
NEW
4030
                                        .as_ref()
×
NEW
4031
                                        .unwrap(),
×
NEW
4032
                                    &[
×
NEW
4033
                                        render_effect_indirect_offset as u32,
×
NEW
4034
                                        render_group_indirect_offset as u32,
×
NEW
4035
                                        render_group_indirect_offset as u32,
×
4036
                                    ],
4037
                                );
NEW
4038
                                compute_pass.dispatch_workgroups(workgroup_count, 1, 1);
×
NEW
4039
                                trace!("init compute dispatched");
×
4040
                            }
4041

4042
                            EffectInitializer::Cloner(EffectCloner {
NEW
4043
                                spawn_this_frame, ..
×
NEW
4044
                            }) => {
×
NEW
4045
                                if !spawn_this_frame {
×
NEW
4046
                                    continue;
×
4047
                                }
4048

NEW
4049
                                let mut compute_pass = render_context
×
4050
                                    .command_encoder()
NEW
4051
                                    .begin_compute_pass(&ComputePassDescriptor {
×
NEW
4052
                                        label: Some("hanabi:clone"),
×
NEW
4053
                                        timestamp_writes: None,
×
4054
                                    });
4055

NEW
4056
                                let clone_pipeline_id = batches.init_and_update_pipeline_ids
×
NEW
4057
                                    [dest_group_index as usize]
×
NEW
4058
                                    .init;
×
4059

NEW
4060
                                let effect_cache_id = batches.effect_cache_id;
×
4061

NEW
4062
                                let Some(clone_pipeline) =
×
4063
                                    pipeline_cache.get_compute_pipeline(clone_pipeline_id)
4064
                                else {
NEW
4065
                                    if let CachedPipelineState::Err(err) =
×
NEW
4066
                                        pipeline_cache.get_compute_pipeline_state(clone_pipeline_id)
×
4067
                                    {
4068
                                        error!(
NEW
4069
                                            "Failed to find clone pipeline #{} for effect \
×
NEW
4070
                                                    {:?}: {:?}",
×
NEW
4071
                                            clone_pipeline_id.id(),
×
4072
                                            entity,
4073
                                            err
4074
                                        );
4075
                                    }
NEW
4076
                                    continue;
×
4077
                                };
4078

NEW
4079
                                let Some(particles_init_bind_group) =
×
4080
                                    effect_cache.init_bind_group(effect_cache_id)
4081
                                else {
NEW
4082
                                    error!(
×
NEW
4083
                                        "Failed to find clone particle buffer bind group \
×
NEW
4084
                                                 for entity {:?}, effect cache ID {:?}",
×
4085
                                        entity, effect_cache_id
4086
                                    );
NEW
4087
                                    continue;
×
4088
                                };
4089

4090
                                let render_effect_dispatch_buffer_index = batches
4091
                                    .dispatch_buffer_indices
4092
                                    .render_effect_metadata_buffer_index;
4093
                                let clone_dest_render_group_dispatch_buffer_index = batches
4094
                                    .dispatch_buffer_indices
4095
                                    .trail_dispatch_buffer_indices[&dest_group_index]
4096
                                    .dest;
4097
                                let clone_src_render_group_dispatch_buffer_index = batches
4098
                                    .dispatch_buffer_indices
4099
                                    .trail_dispatch_buffer_indices[&dest_group_index]
4100
                                    .src;
4101

4102
                                let render_effect_indirect_offset =
4103
                                    effects_meta.gpu_limits.render_effect_indirect_offset(
4104
                                        render_effect_dispatch_buffer_index.0,
4105
                                    );
4106

4107
                                let clone_dest_render_group_indirect_offset =
4108
                                    effects_meta.gpu_limits.render_group_indirect_offset(
4109
                                        clone_dest_render_group_dispatch_buffer_index.0,
4110
                                    );
4111
                                let clone_src_render_group_indirect_offset =
4112
                                    effects_meta.gpu_limits.render_group_indirect_offset(
4113
                                        clone_src_render_group_dispatch_buffer_index.0,
4114
                                    );
4115

4116
                                compute_pass.set_pipeline(clone_pipeline);
4117
                                compute_pass.set_bind_group(
4118
                                    0,
4119
                                    effects_meta.sim_params_bind_group.as_ref().unwrap(),
4120
                                    &[],
4121
                                );
4122
                                compute_pass.set_bind_group(1, particles_init_bind_group, &[]);
4123
                                compute_pass.set_bind_group(
4124
                                    2,
4125
                                    effects_meta.spawner_bind_group.as_ref().unwrap(),
4126
                                    &[spawner_offset],
4127
                                );
4128
                                compute_pass.set_bind_group(
4129
                                    3,
4130
                                    effects_meta
4131
                                        .init_render_indirect_bind_group
4132
                                        .as_ref()
4133
                                        .unwrap(),
4134
                                    &[
4135
                                        render_effect_indirect_offset as u32,
4136
                                        clone_dest_render_group_indirect_offset as u32,
4137
                                        clone_src_render_group_indirect_offset as u32,
4138
                                    ],
4139
                                );
4140

NEW
4141
                                if let Some(dispatch_indirect_buffer) =
×
4142
                                    effects_meta.dispatch_indirect_buffer.buffer()
4143
                                {
4144
                                    compute_pass.dispatch_workgroups_indirect(
4145
                                        dispatch_indirect_buffer,
4146
                                        clone_src_render_group_indirect_offset,
4147
                                    );
4148
                                }
NEW
4149
                                trace!("clone compute dispatched");
×
4150
                            }
4151
                        }
4152
                    }
4153
                }
4154
            }
4155
        }
4156

4157
        // Compute indirect dispatch pass
4158
        if effects_meta.spawner_buffer.buffer().is_some()
10✔
4159
            && !effects_meta.spawner_buffer.is_empty()
×
4160
            && effects_meta.dr_indirect_bind_group.is_some()
×
4161
            && effects_meta.sim_params_bind_group.is_some()
×
4162
        {
4163
            // Only if there's an effect
4164
            let mut compute_pass =
×
4165
                render_context
×
4166
                    .command_encoder()
4167
                    .begin_compute_pass(&ComputePassDescriptor {
×
4168
                        label: Some("hanabi:indirect_dispatch"),
×
4169
                        timestamp_writes: None,
×
4170
                    });
4171

4172
            // Dispatch indirect dispatch compute job
4173
            trace!("record commands for indirect dispatch pipeline...");
×
4174

4175
            // FIXME - The `vfx_indirect` shader assumes a contiguous array of ParticleGroup
4176
            // structures. So we need to pass the full array size, and we
4177
            // just update the unused groups for nothing. Otherwise we might
4178
            // update some unused group and miss some used ones, if there's any gap
4179
            // in the array.
4180
            const WORKGROUP_SIZE: u32 = 64;
4181
            let total_group_count = effects_meta.particle_group_buffer.len() as u32;
×
4182
            let workgroup_count = (total_group_count + WORKGROUP_SIZE - 1) / WORKGROUP_SIZE;
×
4183

4184
            // Setup compute pass
4185
            compute_pass.set_pipeline(&dispatch_indirect_pipeline.pipeline);
×
4186
            compute_pass.set_bind_group(
×
4187
                0,
4188
                // FIXME - got some unwrap() panic here, investigate... possibly race
4189
                // condition!
4190
                effects_meta.dr_indirect_bind_group.as_ref().unwrap(),
×
4191
                &[],
×
4192
            );
4193
            compute_pass.set_bind_group(
×
4194
                1,
4195
                effects_meta.sim_params_bind_group.as_ref().unwrap(),
×
4196
                &[],
×
4197
            );
4198
            compute_pass.dispatch_workgroups(workgroup_count, 1, 1);
×
4199
            trace!(
×
4200
                "indirect dispatch compute dispatched: num_batches={} workgroup_count={}",
×
4201
                total_group_count,
4202
                workgroup_count
4203
            );
4204
        }
4205

4206
        // Compute update pass
4207
        {
4208
            let mut compute_pass =
10✔
4209
                render_context
10✔
4210
                    .command_encoder()
4211
                    .begin_compute_pass(&ComputePassDescriptor {
10✔
4212
                        label: Some("hanabi:update"),
10✔
4213
                        timestamp_writes: None,
10✔
4214
                    });
4215

4216
            // Dispatch update compute jobs
4217
            for (entity, batches) in self.effect_query.iter_manual(world) {
×
4218
                let effect_cache_id = batches.effect_cache_id;
×
4219

4220
                let Some(particles_update_bind_group) =
×
4221
                    effect_cache.update_bind_group(effect_cache_id)
×
4222
                else {
4223
                    error!(
×
4224
                        "Failed to find update particle buffer bind group for entity {:?}, effect cache ID {:?}",
×
4225
                        entity, effect_cache_id
4226
                    );
4227
                    continue;
×
4228
                };
4229

4230
                let first_update_group_dispatch_buffer_index = batches
4231
                    .dispatch_buffer_indices
4232
                    .first_update_group_dispatch_buffer_index;
4233

UNCOV
4234
                let Some(update_render_indirect_bind_group) = &effect_bind_groups
×
4235
                    .update_render_indirect_bind_groups
4236
                    .get(&effect_cache_id)
4237
                else {
4238
                    error!(
×
4239
                        "Failed to find update render indirect bind group for effect cache ID: {:?}, IDs present: {:?}",
×
4240
                        effect_cache_id,
×
4241
                        effect_bind_groups
×
4242
                            .update_render_indirect_bind_groups
×
4243
                            .keys()
×
4244
                            .collect::<Vec<_>>()
×
4245
                    );
4246
                    continue;
×
4247
                };
4248

NEW
4249
                for &group_index in batches.group_order.iter() {
×
NEW
4250
                    let init_and_update_pipeline_id =
×
NEW
4251
                        &batches.init_and_update_pipeline_ids[group_index as usize];
×
4252
                    let Some(update_pipeline) =
×
NEW
4253
                        pipeline_cache.get_compute_pipeline(init_and_update_pipeline_id.update)
×
4254
                    else {
NEW
4255
                        if let CachedPipelineState::Err(err) = pipeline_cache
×
NEW
4256
                            .get_compute_pipeline_state(init_and_update_pipeline_id.update)
×
4257
                        {
4258
                            error!(
4259
                                "Failed to find update pipeline #{} for effect {:?}, group {}: {:?}",
×
NEW
4260
                                init_and_update_pipeline_id.update.id(),
×
4261
                                entity,
4262
                                group_index,
4263
                                err
4264
                            );
4265
                        }
4266
                        continue;
×
4267
                    };
4268

4269
                    let update_group_dispatch_buffer_offset =
4270
                        effects_meta.gpu_limits.dispatch_indirect_offset(
4271
                            first_update_group_dispatch_buffer_index.0 + group_index,
4272
                        );
4273

4274
                    // Destination group spawners are packed one after one another.
4275
                    let spawner_base = batches.spawner_base + group_index;
4276
                    let spawner_buffer_aligned = effects_meta.spawner_buffer.aligned_size();
4277
                    assert!(spawner_buffer_aligned >= GpuSpawnerParams::min_size().get() as usize);
NEW
4278
                    let spawner_offset = spawner_base * spawner_buffer_aligned as u32;
×
4279

4280
                    // for (effect_entity, effect_slice) in effects_meta.entity_map.iter()
4281
                    // Retrieve the ExtractedEffect from the entity
4282
                    // trace!("effect_entity={:?} effect_slice={:?}", effect_entity,
4283
                    // effect_slice); let effect =
4284
                    // self.effect_query.get_manual(world, *effect_entity).unwrap();
4285

4286
                    // Get the slice to update
4287
                    // let effect_slice = effects_meta.get(&effect_entity);
4288
                    // let effect_group =
4289
                    //     &effects_meta.effect_cache.buffers()[batch.buffer_index as usize];
4290

UNCOV
4291
                    trace!(
×
4292
                        "record commands for update pipeline of effect {:?} \
×
4293
                        spawner_base={} update_group_dispatch_buffer_offset={}…",
×
4294
                        batches.handle,
4295
                        spawner_base,
4296
                        update_group_dispatch_buffer_offset,
4297
                    );
4298

4299
                    // Setup compute pass
4300
                    // compute_pass.set_pipeline(&effect_group.update_pipeline);
4301
                    compute_pass.set_pipeline(update_pipeline);
×
4302
                    compute_pass.set_bind_group(
×
4303
                        0,
4304
                        effects_meta.sim_params_bind_group.as_ref().unwrap(),
×
4305
                        &[],
×
4306
                    );
4307
                    compute_pass.set_bind_group(1, particles_update_bind_group, &[]);
×
4308
                    compute_pass.set_bind_group(
×
4309
                        2,
4310
                        effects_meta.spawner_bind_group.as_ref().unwrap(),
×
NEW
4311
                        &[spawner_offset],
×
4312
                    );
4313
                    compute_pass.set_bind_group(3, update_render_indirect_bind_group, &[]);
×
4314

4315
                    if let Some(buffer) = effects_meta.dispatch_indirect_buffer.buffer() {
×
4316
                        trace!(
4317
                            "dispatch_workgroups_indirect: buffer={:?} offset={}",
×
4318
                            buffer,
4319
                            update_group_dispatch_buffer_offset,
4320
                        );
4321
                        compute_pass.dispatch_workgroups_indirect(
×
4322
                            buffer,
×
4323
                            update_group_dispatch_buffer_offset as u64,
×
4324
                        );
4325
                        // TODO - offset
4326
                    }
4327

4328
                    trace!("update compute dispatched");
×
4329
                }
4330
            }
4331
        }
4332

4333
        Ok(())
10✔
4334
    }
4335
}
4336

4337
// FIXME - Remove this, handle it properly with a BufferTable::insert_many() or
4338
// so...
4339
fn allocate_sequential_buffers<T, I>(
×
4340
    buffer_table: &mut BufferTable<T>,
4341
    iterator: I,
4342
) -> BufferTableId
4343
where
4344
    T: Pod + ShaderSize,
4345
    I: Iterator<Item = T>,
4346
{
4347
    let mut first_buffer = None;
×
4348
    for (object_index, object) in iterator.enumerate() {
×
4349
        let buffer = buffer_table.insert(object);
×
4350
        match first_buffer {
×
4351
            None => first_buffer = Some(buffer),
×
4352
            Some(ref first_buffer) => {
×
4353
                if first_buffer.0 + object_index as u32 != buffer.0 {
×
4354
                    error!(
×
4355
                        "Allocator didn't allocate sequential indices (expected {:?}, got {:?}). \
×
4356
                        Expect trouble!",
×
4357
                        first_buffer.0 + object_index as u32,
×
4358
                        buffer.0
×
4359
                    );
4360
                }
4361
            }
4362
        }
4363
    }
4364

4365
    first_buffer.expect("No buffers allocated")
×
4366
}
4367

4368
#[cfg(test)]
4369
mod tests {
4370
    use super::*;
4371

4372
    #[test]
4373
    fn layout_flags() {
4374
        let flags = LayoutFlags::default();
4375
        assert_eq!(flags, LayoutFlags::NONE);
4376
    }
4377

4378
    #[cfg(feature = "gpu_tests")]
4379
    #[test]
4380
    fn gpu_limits() {
4381
        use crate::test_utils::MockRenderer;
4382

4383
        let renderer = MockRenderer::new();
4384
        let device = renderer.device();
4385
        let limits = GpuLimits::from_device(&device);
4386

4387
        // assert!(limits.storage_buffer_align().get() >= 1);
4388
        assert!(
4389
            limits.render_effect_indirect_offset(256)
4390
                >= 256 * GpuRenderEffectMetadata::min_size().get()
4391
        );
4392
        assert!(
4393
            limits.render_group_indirect_offset(256)
4394
                >= 256 * GpuRenderGroupIndirect::min_size().get()
4395
        );
4396
        assert!(
4397
            limits.dispatch_indirect_offset(256) as u64
4398
                >= 256 * GpuDispatchIndirect::min_size().get()
4399
        );
4400
    }
4401
}
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