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

djeedai / bevy_hanabi / 12128238298

02 Dec 2024 09:24PM UTC coverage: 48.661% (-7.6%) from 56.217%
12128238298

Pull #401

github

web-flow
Merge 30c486d1a into 19aee8dbc
Pull Request #401: Upgrade to Bevy v0.15.0

39 of 284 new or added lines in 11 files covered. (13.73%)

435 existing lines in 8 files now uncovered.

3106 of 6383 relevant lines covered (48.66%)

21.61 hits per line

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

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

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

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

70
mod aligned_buffer_vec;
71
mod batch;
72
mod buffer_table;
73
mod effect_cache;
74
mod shader_cache;
75

76
use aligned_buffer_vec::AlignedBufferVec;
77
use buffer_table::{BufferTable, BufferTableId};
78
pub(crate) use effect_cache::{EffectCache, EffectCacheId};
79
pub use shader_cache::ShaderCache;
80

81
use self::batch::EffectBatches;
82

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

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

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

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

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

140
impl Default for GpuSimParams {
UNCOV
141
    fn default() -> Self {
×
142
        Self {
143
            delta_time: 0.04,
144
            time: 0.0,
145
            virtual_delta_time: 0.04,
146
            virtual_time: 0.0,
147
            real_delta_time: 0.04,
148
            real_time: 0.0,
149
            num_groups: 0,
150
        }
151
    }
152
}
153

154
impl From<SimParams> for GpuSimParams {
155
    #[inline]
156
    fn from(src: SimParams) -> Self {
×
157
        Self::from(&src)
×
158
    }
159
}
160

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

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

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

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

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

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

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

245
impl<T: ShaderType> StorageType for T {
246
    fn aligned_size(alignment: u32) -> NonZeroU64 {
25✔
247
        NonZeroU64::new(T::min_size().get().next_multiple_of(alignment as u64)).unwrap()
25✔
248
    }
249

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

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

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

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

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

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

327
/// Indirect draw parameters, with some data of our own tacked on to the end.
328
///
329
/// A few fields of this differ depending on whether the mesh is indexed or
330
/// non-indexed.
331
#[repr(C)]
332
#[derive(Debug, Default, Clone, Copy, Pod, Zeroable, ShaderType)]
333
pub struct GpuRenderGroupIndirect {
334
    /// The number of vertices in the mesh, if non-indexed; if indexed, the
335
    /// number of indices in the mesh.
336
    pub vertex_count: u32,
337
    /// The number of instances to render.
338
    pub instance_count: u32,
339
    /// The first index to render, if the mesh is indexed; the offset of the
340
    /// first vertex, if the mesh is non-indexed.
341
    pub first_index_or_vertex_offset: u32,
342
    /// The offset of the first vertex, if the mesh is indexed; the first
343
    /// instance to render, if the mesh is non-indexed.
344
    pub vertex_offset_or_base_instance: i32,
345
    /// The first instance to render, if indexed; unused if non-indexed.
346
    pub base_instance: u32,
347
    //
348
    pub alive_count: u32,
349
    pub max_update: u32,
350
    pub dead_count: u32,
351
    pub max_spawn: u32,
352
}
353

354
/// Stores metadata about each particle group.
355
///
356
/// This is written by the CPU and read by the GPU.
357
#[repr(C)]
358
#[derive(Debug, Default, Clone, Copy, Pod, Zeroable, ShaderType)]
359
pub struct GpuParticleGroup {
360
    /// The absolute index of this particle group in the global particle group
361
    /// buffer, which includes all effects.
362
    pub global_group_index: u32,
363
    /// The global index of the particle effect.
364
    pub effect_index: u32,
365
    /// The relative index of this particle group in the effect.
366
    ///
367
    /// For example, the first group in an effect has index 0, the second has
368
    /// index 1, etc. This is always 0 when not using groups.
369
    pub group_index_in_effect: u32,
370
    /// The index of the first particle in this group in the indirect index
371
    /// buffer.
372
    pub indirect_index: u32,
373
    /// The capacity of this group, in number of particles.
374
    pub capacity: u32,
375
    /// The index of the first particle in the particle and indirect buffers of
376
    /// this effect.
377
    pub effect_particle_offset: u32,
378
    /// Index of the [`GpuDispatchIndirect`] struct inside the global
379
    /// [`EffectsMeta::dispatch_indirect_buffer`].
380
    pub indirect_dispatch_index: u32,
381
}
382

383
/// Compute pipeline to run the `vfx_indirect` dispatch workgroup calculation
384
/// shader.
385
#[derive(Resource)]
386
pub(crate) struct DispatchIndirectPipeline {
387
    dispatch_indirect_layout: BindGroupLayout,
388
    pipeline: ComputePipeline,
389
}
390

391
impl FromWorld for DispatchIndirectPipeline {
UNCOV
392
    fn from_world(world: &mut World) -> Self {
×
UNCOV
393
        let render_device = world.get_resource::<RenderDevice>().unwrap();
×
394

UNCOV
395
        let storage_alignment = render_device.limits().min_storage_buffer_offset_alignment;
×
UNCOV
396
        let render_effect_indirect_size = GpuRenderEffectMetadata::aligned_size(storage_alignment);
×
UNCOV
397
        let render_group_indirect_size = GpuRenderGroupIndirect::aligned_size(storage_alignment);
×
UNCOV
398
        let dispatch_indirect_size = GpuDispatchIndirect::aligned_size(storage_alignment);
×
UNCOV
399
        let particle_group_size = GpuParticleGroup::aligned_size(storage_alignment);
×
400

UNCOV
401
        trace!(
×
402
            "GpuRenderEffectMetadata: min_size={} padded_size={} | GpuRenderGroupIndirect: min_size={} padded_size={} | \
×
403
            GpuDispatchIndirect: min_size={} padded_size={} | GpuParticleGroup: min_size={} padded_size={}",
×
404
            GpuRenderEffectMetadata::min_size(),
×
405
            render_effect_indirect_size,
×
406
            GpuRenderGroupIndirect::min_size(),
×
407
            render_group_indirect_size,
×
408
            GpuDispatchIndirect::min_size(),
×
409
            dispatch_indirect_size,
×
410
            GpuParticleGroup::min_size(),
×
411
            particle_group_size
412
        );
UNCOV
413
        let dispatch_indirect_layout = render_device.create_bind_group_layout(
×
414
            "hanabi:bind_group_layout:dispatch_indirect_dispatch_indirect",
UNCOV
415
            &[
×
UNCOV
416
                BindGroupLayoutEntry {
×
UNCOV
417
                    binding: 0,
×
UNCOV
418
                    visibility: ShaderStages::COMPUTE,
×
UNCOV
419
                    ty: BindingType::Buffer {
×
UNCOV
420
                        ty: BufferBindingType::Storage { read_only: false },
×
UNCOV
421
                        has_dynamic_offset: false,
×
UNCOV
422
                        min_binding_size: Some(render_effect_indirect_size),
×
423
                    },
UNCOV
424
                    count: None,
×
425
                },
UNCOV
426
                BindGroupLayoutEntry {
×
UNCOV
427
                    binding: 1,
×
UNCOV
428
                    visibility: ShaderStages::COMPUTE,
×
UNCOV
429
                    ty: BindingType::Buffer {
×
UNCOV
430
                        ty: BufferBindingType::Storage { read_only: false },
×
UNCOV
431
                        has_dynamic_offset: false,
×
UNCOV
432
                        min_binding_size: Some(render_group_indirect_size),
×
433
                    },
UNCOV
434
                    count: None,
×
435
                },
UNCOV
436
                BindGroupLayoutEntry {
×
UNCOV
437
                    binding: 2,
×
UNCOV
438
                    visibility: ShaderStages::COMPUTE,
×
UNCOV
439
                    ty: BindingType::Buffer {
×
UNCOV
440
                        ty: BufferBindingType::Storage { read_only: false },
×
UNCOV
441
                        has_dynamic_offset: false,
×
UNCOV
442
                        min_binding_size: Some(dispatch_indirect_size),
×
443
                    },
UNCOV
444
                    count: None,
×
445
                },
UNCOV
446
                BindGroupLayoutEntry {
×
UNCOV
447
                    binding: 3,
×
UNCOV
448
                    visibility: ShaderStages::COMPUTE,
×
UNCOV
449
                    ty: BindingType::Buffer {
×
UNCOV
450
                        ty: BufferBindingType::Storage { read_only: true },
×
UNCOV
451
                        has_dynamic_offset: false,
×
UNCOV
452
                        min_binding_size: Some(particle_group_size),
×
453
                    },
UNCOV
454
                    count: None,
×
455
                },
UNCOV
456
                BindGroupLayoutEntry {
×
UNCOV
457
                    binding: 4,
×
UNCOV
458
                    visibility: ShaderStages::COMPUTE,
×
UNCOV
459
                    ty: BindingType::Buffer {
×
UNCOV
460
                        ty: BufferBindingType::Storage { read_only: true },
×
UNCOV
461
                        has_dynamic_offset: false,
×
UNCOV
462
                        min_binding_size: Some(GpuSpawnerParams::min_size()),
×
463
                    },
UNCOV
464
                    count: None,
×
465
                },
466
            ],
467
        );
468

UNCOV
469
        trace!("GpuSimParams: min_size={}", GpuSimParams::min_size());
×
UNCOV
470
        let sim_params_layout = render_device.create_bind_group_layout(
×
471
            "hanabi:bind_group_layout:dispatch_indirect_sim_params",
UNCOV
472
            &[BindGroupLayoutEntry {
×
UNCOV
473
                binding: 0,
×
UNCOV
474
                visibility: ShaderStages::COMPUTE,
×
UNCOV
475
                ty: BindingType::Buffer {
×
UNCOV
476
                    ty: BufferBindingType::Uniform,
×
UNCOV
477
                    has_dynamic_offset: false,
×
UNCOV
478
                    min_binding_size: Some(GpuSimParams::min_size()),
×
479
                },
UNCOV
480
                count: None,
×
481
            }],
482
        );
483

UNCOV
484
        let pipeline_layout = render_device.create_pipeline_layout(&PipelineLayoutDescriptor {
×
UNCOV
485
            label: Some("hanabi:pipeline_layout:dispatch_indirect"),
×
UNCOV
486
            bind_group_layouts: &[&dispatch_indirect_layout, &sim_params_layout],
×
UNCOV
487
            push_constant_ranges: &[],
×
488
        });
489

UNCOV
490
        let render_effect_indirect_stride_code =
×
UNCOV
491
            (render_effect_indirect_size.get() as u32).to_wgsl_string();
×
UNCOV
492
        let render_group_indirect_stride_code =
×
UNCOV
493
            (render_group_indirect_size.get() as u32).to_wgsl_string();
×
UNCOV
494
        let dispatch_indirect_stride_code = (dispatch_indirect_size.get() as u32).to_wgsl_string();
×
UNCOV
495
        let indirect_code = include_str!("vfx_indirect.wgsl")
×
496
            .replace(
497
                "{{RENDER_EFFECT_INDIRECT_STRIDE}}",
UNCOV
498
                &render_effect_indirect_stride_code,
×
499
            )
500
            .replace(
501
                "{{RENDER_GROUP_INDIRECT_STRIDE}}",
UNCOV
502
                &render_group_indirect_stride_code,
×
503
            )
504
            .replace(
505
                "{{DISPATCH_INDIRECT_STRIDE}}",
UNCOV
506
                &dispatch_indirect_stride_code,
×
507
            );
508

509
        // Resolve imports. Because we don't insert this shader into Bevy' pipeline
510
        // cache, we don't get that part "for free", so we have to do it manually here.
UNCOV
511
        let indirect_naga_module = {
×
512
            let mut composer = Composer::default();
513

514
            // Import bevy_hanabi::vfx_common
515
            {
516
                let common_shader = HanabiPlugin::make_common_shader(
517
                    render_device.limits().min_storage_buffer_offset_alignment,
518
                );
519
                let mut desc: naga_oil::compose::ComposableModuleDescriptor<'_> =
520
                    (&common_shader).into();
521
                desc.shader_defs.insert(
522
                    "SPAWNER_PADDING".to_string(),
523
                    naga_oil::compose::ShaderDefValue::Bool(true),
524
                );
525
                let res = composer.add_composable_module(desc);
526
                assert!(res.is_ok());
527
            }
528

UNCOV
529
            let shader_defs = default();
×
530

UNCOV
531
            match composer.make_naga_module(NagaModuleDescriptor {
×
UNCOV
532
                source: &indirect_code,
×
UNCOV
533
                file_path: "vfx_indirect.wgsl",
×
UNCOV
534
                shader_defs,
×
UNCOV
535
                ..Default::default()
×
536
            }) {
537
                Ok(naga_module) => ShaderSource::Naga(Cow::Owned(naga_module)),
538
                Err(compose_error) => panic!(
×
539
                    "Failed to compose vfx_indirect.wgsl, naga_oil returned: {}",
540
                    compose_error.emit_to_string(&composer)
×
541
                ),
542
            }
543
        };
544

UNCOV
545
        debug!("Create indirect dispatch shader:\n{}", indirect_code);
×
546

UNCOV
547
        let shader_module = render_device.create_shader_module(ShaderModuleDescriptor {
×
UNCOV
548
            label: Some("hanabi:vfx_indirect_shader"),
×
UNCOV
549
            source: indirect_naga_module,
×
550
        });
551

UNCOV
552
        let pipeline = render_device.create_compute_pipeline(&RawComputePipelineDescriptor {
×
UNCOV
553
            label: Some("hanabi:compute_pipeline:dispatch_indirect"),
×
UNCOV
554
            layout: Some(&pipeline_layout),
×
UNCOV
555
            module: &shader_module,
×
NEW
556
            entry_point: Some("main"),
×
UNCOV
557
            compilation_options: default(),
×
NEW
558
            cache: None,
×
559
        });
560

561
        Self {
562
            dispatch_indirect_layout,
563
            pipeline,
564
        }
565
    }
566
}
567

568
#[derive(Resource)]
569
pub(crate) struct ParticlesInitPipeline {
570
    render_device: RenderDevice,
571
    sim_params_layout: BindGroupLayout,
572
    spawner_buffer_layout: BindGroupLayout,
573
    render_indirect_spawn_layout: BindGroupLayout,
574
    render_indirect_clone_layout: BindGroupLayout,
575
}
576

577
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
578
pub(crate) struct ParticleInitPipelineKey {
579
    shader: Handle<Shader>,
580
    particle_layout_min_binding_size: NonZero<u64>,
581
    property_layout_min_binding_size: Option<NonZero<u64>>,
582
    flags: ParticleInitPipelineKeyFlags,
583
}
584

585
bitflags! {
586
    #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
587
    pub struct ParticleInitPipelineKeyFlags: u8 {
588
        const CLONE = 0x1;
589
        const ATTRIBUTE_PREV = 0x2;
590
        const ATTRIBUTE_NEXT = 0x4;
591
    }
592
}
593

594
impl FromWorld for ParticlesInitPipeline {
UNCOV
595
    fn from_world(world: &mut World) -> Self {
×
UNCOV
596
        let render_device = world.get_resource::<RenderDevice>().unwrap();
×
597

UNCOV
598
        let sim_params_layout = render_device.create_bind_group_layout(
×
599
            "hanabi:bind_group_layout:update_sim_params",
UNCOV
600
            &[BindGroupLayoutEntry {
×
UNCOV
601
                binding: 0,
×
UNCOV
602
                visibility: ShaderStages::COMPUTE,
×
UNCOV
603
                ty: BindingType::Buffer {
×
UNCOV
604
                    ty: BufferBindingType::Uniform,
×
UNCOV
605
                    has_dynamic_offset: false,
×
UNCOV
606
                    min_binding_size: Some(GpuSimParams::min_size()),
×
607
                },
UNCOV
608
                count: None,
×
609
            }],
610
        );
611

UNCOV
612
        let spawner_buffer_layout = render_device.create_bind_group_layout(
×
613
            "hanabi:buffer_layout:init_spawner",
UNCOV
614
            &[BindGroupLayoutEntry {
×
UNCOV
615
                binding: 0,
×
UNCOV
616
                visibility: ShaderStages::COMPUTE,
×
UNCOV
617
                ty: BindingType::Buffer {
×
UNCOV
618
                    ty: BufferBindingType::Storage { read_only: false },
×
UNCOV
619
                    has_dynamic_offset: true,
×
UNCOV
620
                    min_binding_size: Some(GpuSpawnerParams::min_size()),
×
621
                },
UNCOV
622
                count: None,
×
623
            }],
624
        );
625

626
        let render_indirect_spawn_layout = create_init_render_indirect_bind_group_layout(
UNCOV
627
            render_device,
×
628
            "hanabi:bind_group_layout:init_render_indirect_spawn",
629
            false,
630
        );
631
        let render_indirect_clone_layout = create_init_render_indirect_bind_group_layout(
UNCOV
632
            render_device,
×
633
            "hanabi:bind_group_layout:init_render_indirect_clone",
634
            true,
635
        );
636

637
        Self {
UNCOV
638
            render_device: render_device.clone(),
×
639
            sim_params_layout,
640
            spawner_buffer_layout,
641
            render_indirect_spawn_layout,
642
            render_indirect_clone_layout,
643
        }
644
    }
645
}
646

647
impl SpecializedComputePipeline for ParticlesInitPipeline {
648
    type Key = ParticleInitPipelineKey;
649

650
    fn specialize(&self, key: Self::Key) -> ComputePipelineDescriptor {
×
651
        let particles_buffer_layout = create_init_particles_bind_group_layout(
652
            &self.render_device,
×
653
            "hanabi:init_particles_buffer_layout",
654
            key.particle_layout_min_binding_size,
×
655
            key.property_layout_min_binding_size,
×
656
        );
657

658
        let mut shader_defs = vec![];
×
659
        if key.flags.contains(ParticleInitPipelineKeyFlags::CLONE) {
×
660
            shader_defs.push(ShaderDefVal::Bool("CLONE".to_string(), true));
×
661
        }
662
        if key
×
663
            .flags
×
664
            .contains(ParticleInitPipelineKeyFlags::ATTRIBUTE_PREV)
×
665
        {
666
            shader_defs.push(ShaderDefVal::Bool("ATTRIBUTE_PREV".to_string(), true));
×
667
        }
668
        if key
×
669
            .flags
×
670
            .contains(ParticleInitPipelineKeyFlags::ATTRIBUTE_NEXT)
×
671
        {
672
            shader_defs.push(ShaderDefVal::Bool("ATTRIBUTE_NEXT".to_string(), true));
×
673
        }
674

675
        let render_indirect_layout = if key.flags.contains(ParticleInitPipelineKeyFlags::CLONE) {
×
676
            self.render_indirect_clone_layout.clone()
×
677
        } else {
678
            self.render_indirect_spawn_layout.clone()
×
679
        };
680

681
        ComputePipelineDescriptor {
682
            label: Some("hanabi:pipeline_init_compute".into()),
×
683
            layout: vec![
×
684
                self.sim_params_layout.clone(),
685
                particles_buffer_layout,
686
                self.spawner_buffer_layout.clone(),
687
                render_indirect_layout,
688
            ],
689
            shader: key.shader,
×
690
            shader_defs,
691
            entry_point: "main".into(),
×
692
            push_constant_ranges: vec![],
×
693
            zero_initialize_workgroup_memory: false,
694
        }
695
    }
696
}
697

698
#[derive(Resource)]
699
pub(crate) struct ParticlesUpdatePipeline {
700
    render_device: RenderDevice,
701
    sim_params_layout: BindGroupLayout,
702
    spawner_buffer_layout: BindGroupLayout,
703
    render_indirect_layout: BindGroupLayout,
704
}
705

706
impl FromWorld for ParticlesUpdatePipeline {
UNCOV
707
    fn from_world(world: &mut World) -> Self {
×
UNCOV
708
        let render_device = world.get_resource::<RenderDevice>().unwrap();
×
709

UNCOV
710
        let limits = render_device.limits();
×
UNCOV
711
        bevy::log::info!(
×
UNCOV
712
            "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={}",
×
713
            limits.max_compute_invocations_per_workgroup, limits.max_compute_workgroup_size_x, limits.max_compute_workgroup_size_y, limits.max_compute_workgroup_size_z,
714
            limits.max_compute_workgroups_per_dimension, limits.min_storage_buffer_offset_alignment
715
        );
716

UNCOV
717
        trace!("GpuSimParams: min_size={}", GpuSimParams::min_size());
×
UNCOV
718
        let sim_params_layout = render_device.create_bind_group_layout(
×
719
            "hanabi:update_sim_params_layout",
UNCOV
720
            &[BindGroupLayoutEntry {
×
UNCOV
721
                binding: 0,
×
UNCOV
722
                visibility: ShaderStages::COMPUTE,
×
UNCOV
723
                ty: BindingType::Buffer {
×
UNCOV
724
                    ty: BufferBindingType::Uniform,
×
UNCOV
725
                    has_dynamic_offset: false,
×
UNCOV
726
                    min_binding_size: Some(GpuSimParams::min_size()),
×
727
                },
UNCOV
728
                count: None,
×
729
            }],
730
        );
731

UNCOV
732
        trace!(
×
733
            "GpuSpawnerParams: min_size={}",
×
734
            GpuSpawnerParams::min_size()
×
735
        );
UNCOV
736
        let spawner_buffer_layout = render_device.create_bind_group_layout(
×
737
            "hanabi:update_spawner_buffer_layout",
UNCOV
738
            &[BindGroupLayoutEntry {
×
UNCOV
739
                binding: 0,
×
UNCOV
740
                visibility: ShaderStages::COMPUTE,
×
UNCOV
741
                ty: BindingType::Buffer {
×
UNCOV
742
                    ty: BufferBindingType::Storage { read_only: false },
×
UNCOV
743
                    has_dynamic_offset: true,
×
UNCOV
744
                    min_binding_size: Some(GpuSpawnerParams::min_size()),
×
745
                },
UNCOV
746
                count: None,
×
747
            }],
748
        );
749

UNCOV
750
        let storage_alignment = render_device.limits().min_storage_buffer_offset_alignment;
×
UNCOV
751
        let render_effect_indirect_size = GpuRenderEffectMetadata::aligned_size(storage_alignment);
×
UNCOV
752
        let render_group_indirect_size = GpuRenderGroupIndirect::aligned_size(storage_alignment);
×
UNCOV
753
        trace!("GpuRenderEffectMetadata: min_size={} padded_size={} | GpuRenderGroupIndirect: min_size={} padded_size={}",
×
754
            GpuRenderEffectMetadata::min_size(),
×
755
            render_effect_indirect_size.get(),
×
756
            GpuRenderGroupIndirect::min_size(),
×
757
            render_group_indirect_size.get());
×
UNCOV
758
        let render_indirect_layout = render_device.create_bind_group_layout(
×
759
            "hanabi:update_render_indirect_layout",
UNCOV
760
            &[
×
UNCOV
761
                BindGroupLayoutEntry {
×
UNCOV
762
                    binding: 0,
×
UNCOV
763
                    visibility: ShaderStages::COMPUTE,
×
UNCOV
764
                    ty: BindingType::Buffer {
×
UNCOV
765
                        ty: BufferBindingType::Storage { read_only: false },
×
UNCOV
766
                        has_dynamic_offset: false,
×
UNCOV
767
                        min_binding_size: Some(render_effect_indirect_size),
×
768
                    },
UNCOV
769
                    count: None,
×
770
                },
UNCOV
771
                BindGroupLayoutEntry {
×
UNCOV
772
                    binding: 1,
×
UNCOV
773
                    visibility: ShaderStages::COMPUTE,
×
UNCOV
774
                    ty: BindingType::Buffer {
×
UNCOV
775
                        ty: BufferBindingType::Storage { read_only: false },
×
UNCOV
776
                        has_dynamic_offset: false,
×
777
                        // Array; needs padded size
UNCOV
778
                        min_binding_size: Some(render_group_indirect_size),
×
779
                    },
UNCOV
780
                    count: None,
×
781
                },
782
            ],
783
        );
784

785
        Self {
UNCOV
786
            render_device: render_device.clone(),
×
787
            sim_params_layout,
788
            spawner_buffer_layout,
789
            render_indirect_layout,
790
        }
791
    }
792
}
793

794
#[derive(Debug, Default, Clone, Hash, PartialEq, Eq)]
795
pub(crate) struct ParticleUpdatePipelineKey {
796
    /// Compute shader, with snippets applied, but not preprocessed yet.
797
    shader: Handle<Shader>,
798
    /// Particle layout.
799
    particle_layout: ParticleLayout,
800
    /// Property layout.
801
    property_layout: PropertyLayout,
802
    is_trail: bool,
803
}
804

805
impl SpecializedComputePipeline for ParticlesUpdatePipeline {
806
    type Key = ParticleUpdatePipelineKey;
807

808
    fn specialize(&self, key: Self::Key) -> ComputePipelineDescriptor {
×
809
        trace!(
×
810
            "GpuParticle: attributes.min_binding_size={} properties.min_binding_size={}",
×
811
            key.particle_layout.min_binding_size().get(),
×
812
            if key.property_layout.is_empty() {
×
813
                0
×
814
            } else {
815
                key.property_layout.min_binding_size().get()
×
816
            },
817
        );
818

819
        let update_particles_buffer_layout = create_update_bind_group_layout(
820
            &self.render_device,
821
            "hanabi:update_particles_buffer_layout",
822
            key.particle_layout.min_binding_size(),
823
            if key.property_layout.is_empty() {
824
                None
×
825
            } else {
826
                Some(key.property_layout.min_binding_size())
×
827
            },
828
        );
829

830
        let mut shader_defs = vec!["REM_MAX_SPAWN_ATOMIC".into()];
831
        if key.particle_layout.contains(Attribute::PREV) {
×
832
            shader_defs.push("ATTRIBUTE_PREV".into());
×
833
        }
834
        if key.particle_layout.contains(Attribute::NEXT) {
×
835
            shader_defs.push("ATTRIBUTE_NEXT".into());
×
836
        }
837
        if key.is_trail {
×
838
            shader_defs.push("TRAIL".into());
×
839
        }
840

841
        ComputePipelineDescriptor {
842
            label: Some("hanabi:pipeline_update_compute".into()),
843
            layout: vec![
844
                self.sim_params_layout.clone(),
845
                update_particles_buffer_layout,
846
                self.spawner_buffer_layout.clone(),
847
                self.render_indirect_layout.clone(),
848
            ],
849
            shader: key.shader,
850
            shader_defs,
851
            entry_point: "main".into(),
852
            push_constant_ranges: Vec::new(),
853
            zero_initialize_workgroup_memory: false,
854
        }
855
    }
856
}
857

858
#[derive(Resource)]
859
pub(crate) struct ParticlesRenderPipeline {
860
    render_device: RenderDevice,
861
    view_layout: BindGroupLayout,
862
    material_layouts: HashMap<TextureLayout, BindGroupLayout>,
863
}
864

865
impl ParticlesRenderPipeline {
866
    /// Cache a material, creating its bind group layout based on the texture
867
    /// layout.
868
    pub fn cache_material(&mut self, layout: &TextureLayout) {
×
869
        if layout.layout.is_empty() {
×
870
            return;
×
871
        }
872

873
        // FIXME - no current stable API to insert an entry into a HashMap only if it
874
        // doesn't exist, and without having to build a key (as opposed to a reference).
875
        // So do 2 lookups instead, to avoid having to clone the layout if it's already
876
        // cached (which should be the common case).
877
        if self.material_layouts.contains_key(layout) {
×
878
            return;
×
879
        }
880

881
        let mut entries = Vec::with_capacity(layout.layout.len() * 2);
×
882
        let mut index = 0;
×
883
        for _slot in &layout.layout {
×
884
            entries.push(BindGroupLayoutEntry {
×
885
                binding: index,
×
886
                visibility: ShaderStages::FRAGMENT,
×
887
                ty: BindingType::Texture {
×
888
                    multisampled: false,
×
889
                    sample_type: TextureSampleType::Float { filterable: true },
×
890
                    view_dimension: TextureViewDimension::D2,
×
891
                },
892
                count: None,
×
893
            });
894
            entries.push(BindGroupLayoutEntry {
×
895
                binding: index + 1,
×
896
                visibility: ShaderStages::FRAGMENT,
×
897
                ty: BindingType::Sampler(SamplerBindingType::Filtering),
×
898
                count: None,
×
899
            });
900
            index += 2;
×
901
        }
902
        debug!(
903
            "Creating material bind group with {} entries [{:?}] for layout {:?}",
×
904
            entries.len(),
×
905
            entries,
906
            layout
907
        );
908
        let material_bind_group_layout = self
×
909
            .render_device
×
910
            .create_bind_group_layout("hanabi:material_layout_render", &entries[..]);
×
911

912
        self.material_layouts
×
913
            .insert(layout.clone(), material_bind_group_layout);
×
914
    }
915

916
    /// Retrieve a bind group layout for a cached material.
917
    pub fn get_material(&self, layout: &TextureLayout) -> Option<&BindGroupLayout> {
×
918
        // Prevent a hash and lookup for the trivial case of an empty layout
919
        if layout.layout.is_empty() {
×
920
            return None;
×
921
        }
922

923
        self.material_layouts.get(layout)
×
924
    }
925
}
926

927
impl FromWorld for ParticlesRenderPipeline {
UNCOV
928
    fn from_world(world: &mut World) -> Self {
×
UNCOV
929
        let render_device = world.get_resource::<RenderDevice>().unwrap();
×
930

UNCOV
931
        let view_layout = render_device.create_bind_group_layout(
×
932
            "hanabi:view_layout_render",
UNCOV
933
            &[
×
UNCOV
934
                BindGroupLayoutEntry {
×
UNCOV
935
                    binding: 0,
×
UNCOV
936
                    visibility: ShaderStages::VERTEX_FRAGMENT,
×
UNCOV
937
                    ty: BindingType::Buffer {
×
UNCOV
938
                        ty: BufferBindingType::Uniform,
×
UNCOV
939
                        has_dynamic_offset: true,
×
UNCOV
940
                        min_binding_size: Some(ViewUniform::min_size()),
×
941
                    },
UNCOV
942
                    count: None,
×
943
                },
UNCOV
944
                BindGroupLayoutEntry {
×
UNCOV
945
                    binding: 1,
×
UNCOV
946
                    visibility: ShaderStages::VERTEX_FRAGMENT,
×
UNCOV
947
                    ty: BindingType::Buffer {
×
UNCOV
948
                        ty: BufferBindingType::Uniform,
×
UNCOV
949
                        has_dynamic_offset: false,
×
UNCOV
950
                        min_binding_size: Some(GpuSimParams::min_size()),
×
951
                    },
UNCOV
952
                    count: None,
×
953
                },
954
            ],
955
        );
956

957
        Self {
UNCOV
958
            render_device: render_device.clone(),
×
959
            view_layout,
UNCOV
960
            material_layouts: default(),
×
961
        }
962
    }
963
}
964

965
#[cfg(all(feature = "2d", feature = "3d"))]
966
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
967
enum PipelineMode {
968
    Camera2d,
969
    Camera3d,
970
}
971

972
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
973
pub(crate) struct ParticleRenderPipelineKey {
974
    /// Render shader, with snippets applied, but not preprocessed yet.
975
    shader: Handle<Shader>,
976
    /// Particle layout.
977
    particle_layout: ParticleLayout,
978
    mesh_layout: Option<MeshVertexBufferLayoutRef>,
979
    /// Texture layout.
980
    texture_layout: TextureLayout,
981
    /// Key: LOCAL_SPACE_SIMULATION
982
    /// The effect is simulated in local space, and during rendering all
983
    /// particles are transformed by the effect's [`GlobalTransform`].
984
    local_space_simulation: bool,
985
    /// Key: USE_ALPHA_MASK, OPAQUE
986
    /// The particle's alpha masking behavior.
987
    alpha_mask: ParticleRenderAlphaMaskPipelineKey,
988
    /// The effect needs Alpha blend.
989
    alpha_mode: AlphaMode,
990
    /// Key: FLIPBOOK
991
    /// The effect is rendered with flipbook texture animation based on the
992
    /// sprite index of each particle.
993
    flipbook: bool,
994
    /// Key: NEEDS_UV
995
    /// The effect needs UVs.
996
    needs_uv: bool,
997
    /// Key: NEEDS_NORMAL
998
    /// The effect needs normals.
999
    needs_normal: bool,
1000
    /// Key: RIBBONS
1001
    /// The effect has ribbons.
1002
    ribbons: bool,
1003
    /// For dual-mode configurations only, the actual mode of the current render
1004
    /// pipeline. Otherwise the mode is implicitly determined by the active
1005
    /// feature.
1006
    #[cfg(all(feature = "2d", feature = "3d"))]
1007
    pipeline_mode: PipelineMode,
1008
    /// MSAA sample count.
1009
    msaa_samples: u32,
1010
    /// Is the camera using an HDR render target?
1011
    hdr: bool,
1012
}
1013

1014
#[derive(Clone, Copy, Default, Hash, PartialEq, Eq, Debug)]
1015
pub(crate) enum ParticleRenderAlphaMaskPipelineKey {
1016
    #[default]
1017
    Blend,
1018
    /// Key: USE_ALPHA_MASK
1019
    /// The effect is rendered with alpha masking.
1020
    AlphaMask,
1021
    /// Key: OPAQUE
1022
    /// The effect is rendered fully-opaquely.
1023
    Opaque,
1024
}
1025

1026
impl Default for ParticleRenderPipelineKey {
1027
    fn default() -> Self {
×
1028
        Self {
1029
            shader: Handle::default(),
×
1030
            particle_layout: ParticleLayout::empty(),
×
1031
            mesh_layout: None,
1032
            texture_layout: default(),
×
1033
            local_space_simulation: false,
1034
            alpha_mask: default(),
×
1035
            alpha_mode: AlphaMode::Blend,
1036
            flipbook: false,
1037
            needs_uv: false,
1038
            needs_normal: false,
1039
            ribbons: false,
1040
            #[cfg(all(feature = "2d", feature = "3d"))]
1041
            pipeline_mode: PipelineMode::Camera3d,
1042
            msaa_samples: Msaa::default().samples(),
×
1043
            hdr: false,
1044
        }
1045
    }
1046
}
1047

1048
impl SpecializedRenderPipeline for ParticlesRenderPipeline {
1049
    type Key = ParticleRenderPipelineKey;
1050

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

1054
        let dispatch_indirect_size = GpuDispatchIndirect::aligned_size(
1055
            self.render_device
×
1056
                .limits()
×
1057
                .min_storage_buffer_offset_alignment,
×
1058
        );
1059
        let mut entries = vec![
×
1060
            BindGroupLayoutEntry {
×
1061
                binding: 0,
×
1062
                visibility: ShaderStages::VERTEX,
×
1063
                ty: BindingType::Buffer {
×
1064
                    ty: BufferBindingType::Storage { read_only: true },
×
1065
                    has_dynamic_offset: false,
×
1066
                    min_binding_size: Some(key.particle_layout.min_binding_size()),
×
1067
                },
1068
                count: None,
×
1069
            },
1070
            BindGroupLayoutEntry {
×
1071
                binding: 1,
×
1072
                visibility: ShaderStages::VERTEX,
×
1073
                ty: BindingType::Buffer {
×
1074
                    ty: BufferBindingType::Storage { read_only: true },
×
1075
                    has_dynamic_offset: false,
×
1076
                    min_binding_size: BufferSize::new(4u64),
×
1077
                },
1078
                count: None,
×
1079
            },
1080
            BindGroupLayoutEntry {
×
1081
                binding: 2,
×
1082
                visibility: ShaderStages::VERTEX,
×
1083
                ty: BindingType::Buffer {
×
1084
                    ty: BufferBindingType::Storage { read_only: true },
×
1085
                    has_dynamic_offset: true,
×
1086
                    min_binding_size: Some(dispatch_indirect_size),
×
1087
                },
1088
                count: None,
×
1089
            },
1090
        ];
1091
        if key.local_space_simulation {
×
1092
            entries.push(BindGroupLayoutEntry {
×
1093
                binding: 3,
×
1094
                visibility: ShaderStages::VERTEX,
×
1095
                ty: BindingType::Buffer {
×
1096
                    ty: BufferBindingType::Storage { read_only: true },
×
1097
                    has_dynamic_offset: true,
×
1098
                    min_binding_size: Some(GpuSpawnerParams::min_size()),
×
1099
                },
1100
                count: None,
×
1101
            });
1102
        }
1103

1104
        trace!(
1105
            "GpuParticle: layout.min_binding_size={}",
×
1106
            key.particle_layout.min_binding_size()
×
1107
        );
1108
        trace!(
×
1109
            "Creating render bind group layout with {} entries",
×
1110
            entries.len()
×
1111
        );
1112
        let particles_buffer_layout = self
×
1113
            .render_device
×
1114
            .create_bind_group_layout("hanabi:buffer_layout_render", &entries);
×
1115

1116
        let mut layout = vec![self.view_layout.clone(), particles_buffer_layout];
×
1117
        let mut shader_defs = vec!["SPAWNER_READONLY".into()];
×
1118

1119
        let vertex_buffer_layout = key.mesh_layout.and_then(|mesh_layout| {
×
1120
            mesh_layout
×
1121
                .0
×
1122
                .get_layout(&[
×
1123
                    Mesh::ATTRIBUTE_POSITION.at_shader_location(0),
×
1124
                    Mesh::ATTRIBUTE_UV_0.at_shader_location(1),
×
1125
                    Mesh::ATTRIBUTE_NORMAL.at_shader_location(2),
×
1126
                ])
1127
                .ok()
×
1128
        });
1129

1130
        if let Some(material_bind_group_layout) = self.get_material(&key.texture_layout) {
×
1131
            layout.push(material_bind_group_layout.clone());
1132
            // //  @location(1) vertex_uv: vec2<f32>
1133
            // vertex_buffer_layout.attributes.push(VertexAttribute {
1134
            //     format: VertexFormat::Float32x2,
1135
            //     offset: 12,
1136
            //     shader_location: 1,
1137
            // });
1138
            // vertex_buffer_layout.array_stride += 8;
1139
        }
1140

1141
        // Key: LOCAL_SPACE_SIMULATION
1142
        if key.local_space_simulation {
×
1143
            shader_defs.push("LOCAL_SPACE_SIMULATION".into());
×
1144
            shader_defs.push("RENDER_NEEDS_SPAWNER".into());
×
1145
        }
1146

1147
        match key.alpha_mask {
1148
            ParticleRenderAlphaMaskPipelineKey::Blend => {}
×
1149
            ParticleRenderAlphaMaskPipelineKey::AlphaMask => {
1150
                // Key: USE_ALPHA_MASK
1151
                shader_defs.push("USE_ALPHA_MASK".into())
×
1152
            }
1153
            ParticleRenderAlphaMaskPipelineKey::Opaque => {
1154
                // Key: OPAQUE
1155
                shader_defs.push("OPAQUE".into())
×
1156
            }
1157
        }
1158

1159
        // Key: FLIPBOOK
1160
        if key.flipbook {
×
1161
            shader_defs.push("FLIPBOOK".into());
×
1162
        }
1163

1164
        // Key: NEEDS_UV
1165
        if key.needs_uv {
×
1166
            shader_defs.push("NEEDS_UV".into());
×
1167
        }
1168

1169
        // Key: NEEDS_NORMAL
1170
        if key.needs_normal {
×
1171
            shader_defs.push("NEEDS_NORMAL".into());
×
1172
        }
1173

1174
        // Key: RIBBONS
1175
        if key.ribbons {
×
1176
            shader_defs.push("RIBBONS".into());
×
1177
        }
1178

1179
        #[cfg(all(feature = "2d", feature = "3d"))]
1180
        assert_eq!(CORE_2D_DEPTH_FORMAT, CORE_3D_DEPTH_FORMAT);
1181
        #[cfg(all(feature = "2d", feature = "3d"))]
UNCOV
1182
        let depth_stencil = match key.pipeline_mode {
×
1183
            // Bevy's Transparent2d render phase doesn't support a depth-stencil buffer.
1184
            PipelineMode::Camera2d => None,
×
1185
            PipelineMode::Camera3d => Some(DepthStencilState {
1186
                format: CORE_3D_DEPTH_FORMAT,
1187
                // Use depth buffer with alpha-masked or opaque particles, not
1188
                // with transparent ones
1189
                depth_write_enabled: matches!(
×
1190
                    key.alpha_mask,
×
1191
                    ParticleRenderAlphaMaskPipelineKey::AlphaMask
1192
                        | ParticleRenderAlphaMaskPipelineKey::Opaque
1193
                ),
1194
                // Bevy uses reverse-Z, so GreaterEqual really means closer
NEW
1195
                depth_compare: CompareFunction::GreaterEqual,
×
1196
                stencil: StencilState::default(),
×
1197
                bias: DepthBiasState::default(),
×
1198
            }),
1199
        };
1200

1201
        #[cfg(all(feature = "2d", not(feature = "3d")))]
1202
        let depth_stencil = Some(DepthStencilState {
1203
            format: CORE_2D_DEPTH_FORMAT,
1204
            // Use depth buffer with alpha-masked particles, not with transparent ones
1205
            depth_write_enabled: false, // TODO - opaque/alphamask 2d
1206
            // Bevy uses reverse-Z, so GreaterEqual really means closer
1207
            depth_compare: CompareFunction::GreaterEqual,
1208
            stencil: StencilState::default(),
1209
            bias: DepthBiasState::default(),
1210
        });
1211

1212
        #[cfg(all(feature = "3d", not(feature = "2d")))]
1213
        let depth_stencil = Some(DepthStencilState {
1214
            format: CORE_3D_DEPTH_FORMAT,
1215
            // Use depth buffer with alpha-masked particles, not with transparent ones
1216
            depth_write_enabled: matches!(
1217
                key.alpha_mask,
1218
                ParticleRenderAlphaMaskPipelineKey::AlphaMask
1219
                    | ParticleRenderAlphaMaskPipelineKey::Opaque
1220
            ),
1221
            // Bevy uses reverse-Z, so GreaterEqual really means closer
1222
            depth_compare: CompareFunction::GreaterEqual,
1223
            stencil: StencilState::default(),
1224
            bias: DepthBiasState::default(),
1225
        });
1226

UNCOV
1227
        let format = if key.hdr {
×
1228
            ViewTarget::TEXTURE_FORMAT_HDR
×
1229
        } else {
1230
            TextureFormat::bevy_default()
×
1231
        };
1232

1233
        RenderPipelineDescriptor {
UNCOV
1234
            vertex: VertexState {
×
1235
                shader: key.shader.clone(),
1236
                entry_point: "vertex".into(),
1237
                shader_defs: shader_defs.clone(),
1238
                buffers: vec![vertex_buffer_layout.expect("Vertex buffer layout not present")],
1239
            },
UNCOV
1240
            fragment: Some(FragmentState {
×
1241
                shader: key.shader,
1242
                shader_defs,
1243
                entry_point: "fragment".into(),
1244
                targets: vec![Some(ColorTargetState {
1245
                    format,
1246
                    blend: Some(key.alpha_mode.into()),
1247
                    write_mask: ColorWrites::ALL,
1248
                })],
1249
            }),
1250
            layout,
UNCOV
1251
            primitive: PrimitiveState {
×
1252
                front_face: FrontFace::Ccw,
1253
                cull_mode: None,
1254
                unclipped_depth: false,
1255
                polygon_mode: PolygonMode::Fill,
1256
                conservative: false,
1257
                topology: PrimitiveTopology::TriangleList,
1258
                strip_index_format: None,
1259
            },
1260
            depth_stencil,
UNCOV
1261
            multisample: MultisampleState {
×
1262
                count: key.msaa_samples,
1263
                mask: !0,
1264
                alpha_to_coverage_enabled: false,
1265
            },
UNCOV
1266
            label: Some("hanabi:pipeline_render".into()),
×
UNCOV
1267
            push_constant_ranges: Vec::new(),
×
1268
            zero_initialize_workgroup_memory: false,
1269
        }
1270
    }
1271
}
1272

1273
/// A single effect instance extracted from a [`ParticleEffect`] as a
1274
/// render world item.
1275
///
1276
/// [`ParticleEffect`]: crate::ParticleEffect
1277
#[derive(Debug, Component)]
1278
pub(crate) struct ExtractedEffect {
1279
    /// Handle to the effect asset this instance is based on.
1280
    /// The handle is weak to prevent refcount cycles and gracefully handle
1281
    /// assets unloaded or destroyed after a draw call has been submitted.
1282
    pub handle: Handle<EffectAsset>,
1283
    /// Particle layout for the effect.
1284
    #[allow(dead_code)]
1285
    pub particle_layout: ParticleLayout,
1286
    /// Property layout for the effect.
1287
    pub property_layout: PropertyLayout,
1288
    /// Values of properties written in a binary blob according to
1289
    /// [`property_layout`].
1290
    ///
1291
    /// This is `Some(blob)` if the data needs to be (re)uploaded to GPU, or
1292
    /// `None` if nothing needs to be done for this frame.
1293
    ///
1294
    /// [`property_layout`]: crate::render::ExtractedEffect::property_layout
1295
    pub property_data: Option<Vec<u8>>,
1296
    /// Maps a group number to the runtime initializer for that group.
1297
    ///
1298
    /// Obtained from calling [`EffectSpawner::tick()`] on the source effect
1299
    /// instance.
1300
    ///
1301
    /// [`EffectSpawner::tick()`]: crate::EffectSpawner::tick
1302
    pub initializers: Vec<EffectInitializer>,
1303
    /// Global transform of the effect origin, extracted from the
1304
    /// [`GlobalTransform`].
1305
    pub transform: Mat4,
1306
    /// Inverse global transform of the effect origin, extracted from the
1307
    /// [`GlobalTransform`].
1308
    pub inverse_transform: Mat4,
1309
    /// Layout flags.
1310
    pub layout_flags: LayoutFlags,
1311
    pub mesh: Handle<Mesh>,
1312
    /// Texture layout.
1313
    pub texture_layout: TextureLayout,
1314
    /// Textures.
1315
    pub textures: Vec<Handle<Image>>,
1316
    /// Alpha mode.
1317
    pub alpha_mode: AlphaMode,
1318
    /// Effect shaders.
1319
    pub effect_shaders: Vec<EffectShader>,
1320
    /// For 2D rendering, the Z coordinate used as the sort key. Ignored for 3D
1321
    /// rendering.
1322
    #[cfg(feature = "2d")]
1323
    pub z_sort_key_2d: FloatOrd,
1324
}
1325

1326
/// Extracted data for newly-added [`ParticleEffect`] component requiring a new
1327
/// GPU allocation.
1328
///
1329
/// [`ParticleEffect`]: crate::ParticleEffect
1330
pub struct AddedEffect {
1331
    /// Entity with a newly-added [`ParticleEffect`] component.
1332
    ///
1333
    /// [`ParticleEffect`]: crate::ParticleEffect
1334
    pub entity: Entity,
1335
    pub groups: Vec<AddedEffectGroup>,
1336
    /// Layout of particle attributes.
1337
    pub particle_layout: ParticleLayout,
1338
    /// Layout of properties for the effect, if properties are used at all, or
1339
    /// an empty layout.
1340
    pub property_layout: PropertyLayout,
1341
    pub layout_flags: LayoutFlags,
1342
    /// Handle of the effect asset.
1343
    pub handle: Handle<EffectAsset>,
1344
    /// The order in which we evaluate groups.
1345
    pub group_order: Vec<u32>,
1346
}
1347

1348
pub struct AddedEffectGroup {
1349
    pub capacity: u32,
1350
    pub src_group_index_if_trail: Option<u32>,
1351
}
1352

1353
/// Collection of all extracted effects for this frame, inserted into the
1354
/// render world as a render resource.
1355
#[derive(Default, Resource)]
1356
pub(crate) struct ExtractedEffects {
1357
    /// Map of extracted effects from the entity the source [`ParticleEffect`]
1358
    /// is on.
1359
    ///
1360
    /// [`ParticleEffect`]: crate::ParticleEffect
1361
    pub effects: HashMap<Entity, ExtractedEffect>,
1362
    /// Entites which had their [`ParticleEffect`] component removed.
1363
    ///
1364
    /// [`ParticleEffect`]: crate::ParticleEffect
1365
    pub removed_effect_entities: Vec<Entity>,
1366
    /// Newly added effects without a GPU allocation yet.
1367
    pub added_effects: Vec<AddedEffect>,
1368
}
1369

1370
#[derive(Default, Resource)]
1371
pub(crate) struct EffectAssetEvents {
1372
    pub images: Vec<AssetEvent<Image>>,
1373
}
1374

1375
/// System extracting all the asset events for the [`Image`] assets to enable
1376
/// dynamic update of images bound to any effect.
1377
///
1378
/// This system runs in parallel of [`extract_effects`].
UNCOV
1379
pub(crate) fn extract_effect_events(
×
1380
    mut events: ResMut<EffectAssetEvents>,
1381
    mut image_events: Extract<EventReader<AssetEvent<Image>>>,
1382
) {
UNCOV
1383
    trace!("extract_effect_events");
×
1384

UNCOV
1385
    let EffectAssetEvents { ref mut images } = *events;
×
UNCOV
1386
    *images = image_events.read().copied().collect();
×
1387
}
1388

1389
/// System extracting data for rendering of all active [`ParticleEffect`]
1390
/// components.
1391
///
1392
/// Extract rendering data for all [`ParticleEffect`] components in the world
1393
/// which are visible ([`ComputedVisibility::is_visible`] is `true`), and wrap
1394
/// the data into a new [`ExtractedEffect`] instance added to the
1395
/// [`ExtractedEffects`] resource.
1396
///
1397
/// This system runs in parallel of [`extract_effect_events`].
1398
///
1399
/// [`ParticleEffect`]: crate::ParticleEffect
UNCOV
1400
pub(crate) fn extract_effects(
×
1401
    real_time: Extract<Res<Time<Real>>>,
1402
    virtual_time: Extract<Res<Time<Virtual>>>,
1403
    time: Extract<Res<Time<EffectSimulation>>>,
1404
    effects: Extract<Res<Assets<EffectAsset>>>,
1405
    mut query: Extract<
1406
        ParamSet<(
1407
            // All existing ParticleEffect components
1408
            Query<(
1409
                Entity,
1410
                Option<&InheritedVisibility>,
1411
                Option<&ViewVisibility>,
1412
                &EffectInitializers,
1413
                &CompiledParticleEffect,
1414
                Option<Ref<EffectProperties>>,
1415
                &GlobalTransform,
1416
            )>,
1417
            // Newly added ParticleEffect components
1418
            Query<
1419
                (Entity, &CompiledParticleEffect),
1420
                (Added<CompiledParticleEffect>, With<GlobalTransform>),
1421
            >,
1422
        )>,
1423
    >,
1424
    mut removed_effects_event_reader: Extract<EventReader<RemovedEffectsEvent>>,
1425
    mut sim_params: ResMut<SimParams>,
1426
    mut extracted_effects: ResMut<ExtractedEffects>,
1427
    effects_meta: Res<EffectsMeta>,
1428
) {
UNCOV
1429
    trace!("extract_effects");
×
1430

1431
    // Save simulation params into render world
NEW
1432
    sim_params.time = time.elapsed_secs_f64();
×
NEW
1433
    sim_params.delta_time = time.delta_secs();
×
NEW
1434
    sim_params.virtual_time = virtual_time.elapsed_secs_f64();
×
NEW
1435
    sim_params.virtual_delta_time = virtual_time.delta_secs();
×
NEW
1436
    sim_params.real_time = real_time.elapsed_secs_f64();
×
NEW
1437
    sim_params.real_delta_time = real_time.delta_secs();
×
1438

1439
    // Collect removed effects for later GPU data purge
UNCOV
1440
    extracted_effects.removed_effect_entities =
×
UNCOV
1441
        removed_effects_event_reader
×
UNCOV
1442
            .read()
×
UNCOV
1443
            .fold(vec![], |mut acc, ev| {
×
1444
                // FIXME - Need to clone because we can't consume the event, we only have
1445
                // read-only access to the main world
1446
                acc.append(&mut ev.entities.clone());
×
1447
                acc
×
1448
            });
1449
    trace!(
1450
        "Found {} removed effect(s).",
×
1451
        extracted_effects.removed_effect_entities.len()
×
1452
    );
1453

1454
    // Collect added effects for later GPU data allocation
UNCOV
1455
    extracted_effects.added_effects = query
×
UNCOV
1456
        .p1()
×
UNCOV
1457
        .iter()
×
UNCOV
1458
        .filter_map(|(entity, compiled_effect)| {
×
UNCOV
1459
            let handle = compiled_effect.asset.clone_weak();
×
UNCOV
1460
            let asset = effects.get(&compiled_effect.asset)?;
×
1461
            let particle_layout = asset.particle_layout();
1462
            assert!(
1463
                particle_layout.size() > 0,
1464
                "Invalid empty particle layout for effect '{}' on entity {:?}. Did you forget to add some modifier to the asset?",
×
1465
                asset.name,
1466
                entity
1467
            );
1468
            let property_layout = asset.property_layout();
×
1469
            let group_order = asset.calculate_group_order();
×
1470

1471
            trace!(
×
1472
                "Found new effect: entity {:?} | capacities {:?} | particle_layout {:?} | \
×
1473
                 property_layout {:?} | layout_flags {:?}",
×
1474
                 entity,
×
1475
                 asset.capacities(),
×
1476
                 particle_layout,
1477
                 property_layout,
1478
                 compiled_effect.layout_flags);
1479

UNCOV
1480
            Some(AddedEffect {
×
1481
                entity,
×
1482
                groups: asset.capacities().iter().zip(asset.init.iter()).map(|(&capacity, init)| {
×
1483
                    AddedEffectGroup {
×
1484
                        capacity,
×
1485
                        src_group_index_if_trail: match init {
×
1486
                            Initializer::Spawner(_) => None,
×
1487
                            Initializer::Cloner(cloner) => Some(cloner.src_group_index),
×
1488
                        }
1489
                    }
1490
                }).collect(),
×
1491
                particle_layout,
1492
                property_layout,
1493
                group_order,
1494
                layout_flags: compiled_effect.layout_flags,
1495
                handle,
1496
            })
1497
        })
1498
        .collect();
1499

1500
    // Loop over all existing effects to extract them
1501
    extracted_effects.effects.clear();
1502
    for (
1503
        entity,
×
1504
        maybe_inherited_visibility,
×
1505
        maybe_view_visibility,
×
1506
        initializers,
×
1507
        effect,
×
1508
        maybe_properties,
×
1509
        transform,
×
1510
    ) in query.p0().iter_mut()
1511
    {
1512
        // Check if shaders are configured
1513
        let effect_shaders = effect.get_configured_shaders();
×
1514
        if effect_shaders.is_empty() {
×
1515
            continue;
×
1516
        }
1517

1518
        // Check if hidden, unless always simulated
1519
        if effect.simulation_condition == SimulationCondition::WhenVisible
×
1520
            && !maybe_inherited_visibility
×
1521
                .map(|cv| cv.get())
×
1522
                .unwrap_or(true)
×
1523
            && !maybe_view_visibility.map(|cv| cv.get()).unwrap_or(true)
×
1524
        {
1525
            continue;
×
1526
        }
1527

1528
        // Check if asset is available, otherwise silently ignore
1529
        let Some(asset) = effects.get(&effect.asset) else {
×
1530
            trace!(
×
1531
                "EffectAsset not ready; skipping ParticleEffect instance on entity {:?}.",
×
1532
                entity
1533
            );
1534
            continue;
×
1535
        };
1536

1537
        #[cfg(feature = "2d")]
1538
        let z_sort_key_2d = effect.z_layer_2d;
1539

1540
        let property_layout = asset.property_layout();
1541
        let texture_layout = asset.module().texture_layout();
1542

1543
        let property_data = if let Some(properties) = maybe_properties {
×
1544
            // Note: must check that property layout is not empty, because the
1545
            // EffectProperties component is marked as changed when added but contains an
1546
            // empty Vec if there's no property, which would later raise an error if we
1547
            // don't return None here.
1548
            if properties.is_changed() && !property_layout.is_empty() {
×
1549
                trace!("Detected property change, re-serializing...");
×
1550
                Some(properties.serialize(&property_layout))
×
1551
            } else {
1552
                None
×
1553
            }
1554
        } else {
1555
            None
×
1556
        };
1557

1558
        let layout_flags = effect.layout_flags;
1559
        let mesh = match effect.mesh {
1560
            None => effects_meta.default_mesh.clone(),
×
1561
            Some(ref mesh) => (*mesh).clone(),
×
1562
        };
1563
        let alpha_mode = effect.alpha_mode;
1564

1565
        trace!(
1566
            "Extracted instance of effect '{}' on entity {:?}: texture_layout_count={} texture_count={} layout_flags={:?}",
×
1567
            asset.name,
×
1568
            entity,
×
1569
            texture_layout.layout.len(),
×
1570
            effect.textures.len(),
×
1571
            layout_flags,
1572
        );
1573

1574
        extracted_effects.effects.insert(
×
1575
            entity,
×
1576
            ExtractedEffect {
×
1577
                handle: effect.asset.clone_weak(),
×
1578
                particle_layout: asset.particle_layout().clone(),
×
1579
                property_layout,
×
1580
                property_data,
×
1581
                initializers: initializers.0.clone(),
×
1582
                transform: transform.compute_matrix(),
×
1583
                // TODO - more efficient/correct way than inverse()?
1584
                inverse_transform: transform.compute_matrix().inverse(),
×
1585
                layout_flags,
×
1586
                mesh,
×
1587
                texture_layout,
×
1588
                textures: effect.textures.clone(),
×
1589
                alpha_mode,
×
1590
                effect_shaders: effect_shaders.to_vec(),
×
1591
                #[cfg(feature = "2d")]
×
1592
                z_sort_key_2d,
×
1593
            },
1594
        );
1595
    }
1596
}
1597

1598
/// Various GPU limits and aligned sizes computed once and cached.
1599
struct GpuLimits {
1600
    /// Value of [`WgpuLimits::min_storage_buffer_offset_alignment`].
1601
    ///
1602
    /// [`WgpuLimits::min_storage_buffer_offset_alignment`]: bevy::render::settings::WgpuLimits::min_storage_buffer_offset_alignment
1603
    storage_buffer_align: NonZeroU32,
1604

1605
    /// Size of [`GpuDispatchIndirect`] aligned to the contraint of
1606
    /// [`WgpuLimits::min_storage_buffer_offset_alignment`].
1607
    ///
1608
    /// [`WgpuLimits::min_storage_buffer_offset_alignment`]: bevy::render::settings::WgpuLimits::min_storage_buffer_offset_alignment
1609
    dispatch_indirect_aligned_size: NonZeroU32,
1610

1611
    /// Size of [`GpuRenderEffectMetadata`] aligned to the contraint of
1612
    /// [`WgpuLimits::min_storage_buffer_offset_alignment`].
1613
    ///
1614
    /// [`WgpuLimits::min_storage_buffer_offset_alignment`]: bevy::render::settings::WgpuLimits::min_storage_buffer_offset_alignment
1615
    render_effect_indirect_aligned_size: NonZeroU32,
1616

1617
    /// Size of [`GpuRenderGroupIndirect`] aligned to the contraint of
1618
    /// [`WgpuLimits::min_storage_buffer_offset_alignment`].
1619
    ///
1620
    /// [`WgpuLimits::min_storage_buffer_offset_alignment`]: bevy::render::settings::WgpuLimits::min_storage_buffer_offset_alignment
1621
    render_group_indirect_aligned_size: NonZeroU32,
1622

1623
    /// Size of [`GpuParticleGroup`] aligned to the contraint of
1624
    /// [`WgpuLimits::min_storage_buffer_offset_alignment`].
1625
    ///
1626
    /// [`WgpuLimits::min_storage_buffer_offset_alignment`]: bevy::render::settings::WgpuLimits::min_storage_buffer_offset_alignment
1627
    particle_group_aligned_size: NonZeroU32,
1628
}
1629

1630
impl GpuLimits {
1631
    pub fn from_device(render_device: &RenderDevice) -> Self {
1✔
1632
        let storage_buffer_align =
1✔
1633
            render_device.limits().min_storage_buffer_offset_alignment as u64;
1✔
1634

1635
        let dispatch_indirect_aligned_size = NonZeroU32::new(
1636
            GpuDispatchIndirect::min_size()
1✔
1637
                .get()
1✔
1638
                .next_multiple_of(storage_buffer_align) as u32,
1✔
1639
        )
1640
        .unwrap();
1641

1642
        let render_effect_indirect_aligned_size = NonZeroU32::new(
1643
            GpuRenderEffectMetadata::min_size()
1✔
1644
                .get()
1✔
1645
                .next_multiple_of(storage_buffer_align) as u32,
1✔
1646
        )
1647
        .unwrap();
1648

1649
        let render_group_indirect_aligned_size = NonZeroU32::new(
1650
            GpuRenderGroupIndirect::min_size()
1✔
1651
                .get()
1✔
1652
                .next_multiple_of(storage_buffer_align) as u32,
1✔
1653
        )
1654
        .unwrap();
1655

1656
        let particle_group_aligned_size = NonZeroU32::new(
1657
            GpuParticleGroup::min_size()
1✔
1658
                .get()
1✔
1659
                .next_multiple_of(storage_buffer_align) as u32,
1✔
1660
        )
1661
        .unwrap();
1662

1663
        trace!(
1✔
1664
            "GpuLimits: storage_buffer_align={} gpu_dispatch_indirect_aligned_size={} \
×
1665
            gpu_render_effect_indirect_aligned_size={} gpu_render_group_indirect_aligned_size={}",
×
1666
            storage_buffer_align,
×
1667
            dispatch_indirect_aligned_size.get(),
×
1668
            render_effect_indirect_aligned_size.get(),
×
1669
            render_group_indirect_aligned_size.get()
×
1670
        );
1671

1672
        Self {
1673
            storage_buffer_align: NonZeroU32::new(storage_buffer_align as u32).unwrap(),
1✔
1674
            dispatch_indirect_aligned_size,
1675
            render_effect_indirect_aligned_size,
1676
            render_group_indirect_aligned_size,
1677
            particle_group_aligned_size,
1678
        }
1679
    }
1680

1681
    /// Byte alignment for any storage buffer binding.
UNCOV
1682
    pub fn storage_buffer_align(&self) -> NonZeroU32 {
×
UNCOV
1683
        self.storage_buffer_align
×
1684
    }
1685

1686
    /// Byte alignment for [`GpuDispatchIndirect`].
1687
    pub fn dispatch_indirect_offset(&self, buffer_index: u32) -> u32 {
1✔
1688
        self.dispatch_indirect_aligned_size.get() * buffer_index
1✔
1689
    }
1690

1691
    /// Byte offset of the [`GpuRenderEffectMetadata`] of a given buffer.
1692
    pub fn render_effect_indirect_offset(&self, buffer_index: u32) -> u64 {
1✔
1693
        self.render_effect_indirect_aligned_size.get() as u64 * buffer_index as u64
1✔
1694
    }
1695

1696
    /// Byte alignment for [`GpuRenderEffectMetadata`].
1697
    pub fn render_effect_indirect_size(&self) -> NonZeroU64 {
×
1698
        NonZeroU64::new(self.render_effect_indirect_aligned_size.get() as u64).unwrap()
×
1699
    }
1700

1701
    /// Byte offset for the [`GpuRenderGroupIndirect`] of a given buffer.
1702
    pub fn render_group_indirect_offset(&self, buffer_index: u32) -> u64 {
1✔
1703
        self.render_group_indirect_aligned_size.get() as u64 * buffer_index as u64
1✔
1704
    }
1705

1706
    /// Byte alignment for [`GpuRenderGroupIndirect`].
1707
    pub fn render_group_indirect_size(&self) -> NonZeroU64 {
×
1708
        NonZeroU64::new(self.render_group_indirect_aligned_size.get() as u64).unwrap()
×
1709
    }
1710

1711
    /// Byte offset for the [`GpuParticleGroup`] of a given buffer.
1712
    pub fn particle_group_offset(&self, buffer_index: u32) -> u32 {
×
1713
        self.particle_group_aligned_size.get() * buffer_index
×
1714
    }
1715
}
1716

1717
/// Global resource containing the GPU data to draw all the particle effects in
1718
/// all views.
1719
///
1720
/// The resource is populated by [`prepare_effects()`] with all the effects to
1721
/// render for the current frame, for all views in the frame, and consumed by
1722
/// [`queue_effects()`] to actually enqueue the drawning commands to draw those
1723
/// effects.
1724
#[derive(Resource)]
1725
pub struct EffectsMeta {
1726
    /// Map from an entity of the main world with a [`ParticleEffect`] component
1727
    /// attached to it, to the associated effect slice allocated in the
1728
    /// [`EffectCache`].
1729
    ///
1730
    /// [`ParticleEffect`]: crate::ParticleEffect
1731
    entity_map: HashMap<Entity, EffectCacheId>,
1732
    /// Bind group for the camera view, containing the camera projection and
1733
    /// other uniform values related to the camera.
1734
    view_bind_group: Option<BindGroup>,
1735
    /// Bind group for the simulation parameters, like the current time and
1736
    /// frame delta time.
1737
    sim_params_bind_group: Option<BindGroup>,
1738
    /// Bind group for the spawning parameters (number of particles to spawn
1739
    /// this frame, ...).
1740
    spawner_bind_group: Option<BindGroup>,
1741
    /// Bind group #0 of the vfx_indirect shader, containing both the indirect
1742
    /// compute dispatch and render buffers.
1743
    dr_indirect_bind_group: Option<BindGroup>,
1744
    /// Bind group #3 of the vfx_init shader, containing the indirect render
1745
    /// buffer, in the case of a spawner with no source buffer.
1746
    init_render_indirect_spawn_bind_group: Option<BindGroup>,
1747
    /// Bind group #3 of the vfx_init shader, containing the indirect render
1748
    /// buffer, in the case of a cloner with a source buffer.
1749
    init_render_indirect_clone_bind_group: Option<BindGroup>,
1750
    /// Global shared GPU uniform buffer storing the simulation parameters,
1751
    /// uploaded each frame from CPU to GPU.
1752
    sim_params_uniforms: UniformBuffer<GpuSimParams>,
1753
    /// Global shared GPU buffer storing the various spawner parameter structs
1754
    /// for the active effect instances.
1755
    spawner_buffer: AlignedBufferVec<GpuSpawnerParams>,
1756
    /// Global shared GPU buffer storing the various indirect dispatch structs
1757
    /// for the indirect dispatch of the Update pass.
1758
    dispatch_indirect_buffer: BufferTable<GpuDispatchIndirect>,
1759
    /// Global shared GPU buffer storing the various `RenderEffectMetadata`
1760
    /// structs for the active effect instances.
1761
    render_effect_dispatch_buffer: BufferTable<GpuRenderEffectMetadata>,
1762
    /// Stores the GPU `RenderGroupIndirect` structures, which describe mutable
1763
    /// data specific to a particle group.
1764
    ///
1765
    /// These structures also store the data needed for indirect dispatch of
1766
    /// drawcalls.
1767
    render_group_dispatch_buffer: BufferTable<GpuRenderGroupIndirect>,
1768
    /// Stores the GPU `ParticleGroup` structures, which are metadata describing
1769
    /// each particle group that's populated by the CPU and read (only read) by
1770
    /// the GPU.
1771
    particle_group_buffer: AlignedBufferVec<GpuParticleGroup>,
1772
    /// The mesh used when particle effects don't specify one (i.e. a quad).
1773
    default_mesh: Handle<Mesh>,
1774
    /// Various GPU limits and aligned sizes lazily allocated and cached for
1775
    /// convenience.
1776
    gpu_limits: GpuLimits,
1777
}
1778

1779
impl EffectsMeta {
UNCOV
1780
    pub fn new(device: RenderDevice, mesh_assets: &mut Assets<Mesh>) -> Self {
×
UNCOV
1781
        let default_mesh = mesh_assets.add(Plane3d::new(Vec3::Z, Vec2::splat(0.5)));
×
1782

UNCOV
1783
        let gpu_limits = GpuLimits::from_device(&device);
×
1784

1785
        // Ensure individual GpuSpawnerParams elements are properly aligned so they can
1786
        // be addressed individually by the computer shaders.
UNCOV
1787
        let item_align = gpu_limits.storage_buffer_align().get() as u64;
×
UNCOV
1788
        trace!(
×
1789
            "Aligning storage buffers to {} bytes as device limits requires.",
×
1790
            item_align
1791
        );
1792

1793
        Self {
UNCOV
1794
            entity_map: HashMap::default(),
×
1795
            view_bind_group: None,
1796
            sim_params_bind_group: None,
1797
            spawner_bind_group: None,
1798
            dr_indirect_bind_group: None,
1799
            init_render_indirect_spawn_bind_group: None,
1800
            init_render_indirect_clone_bind_group: None,
UNCOV
1801
            sim_params_uniforms: UniformBuffer::default(),
×
UNCOV
1802
            spawner_buffer: AlignedBufferVec::new(
×
1803
                BufferUsages::STORAGE,
1804
                NonZeroU64::new(item_align),
1805
                Some("hanabi:buffer:spawner".to_string()),
1806
            ),
UNCOV
1807
            dispatch_indirect_buffer: BufferTable::new(
×
1808
                BufferUsages::STORAGE | BufferUsages::INDIRECT,
1809
                // NOTE: Technically we're using an offset in dispatch_workgroups_indirect(), but
1810
                // `min_storage_buffer_offset_alignment` is documented as being for the offset in
1811
                // BufferBinding and the dynamic offset in set_bind_group(), so either the
1812
                // documentation is lacking or we don't need to align here.
1813
                NonZeroU64::new(item_align),
1814
                Some("hanabi:buffer:dispatch_indirect".to_string()),
1815
            ),
UNCOV
1816
            render_effect_dispatch_buffer: BufferTable::new(
×
1817
                BufferUsages::STORAGE | BufferUsages::INDIRECT,
1818
                NonZeroU64::new(item_align),
1819
                Some("hanabi:buffer:render_effect_dispatch".to_string()),
1820
            ),
UNCOV
1821
            render_group_dispatch_buffer: BufferTable::new(
×
1822
                BufferUsages::STORAGE | BufferUsages::INDIRECT,
1823
                NonZeroU64::new(item_align),
1824
                Some("hanabi:buffer:render_group_dispatch".to_string()),
1825
            ),
UNCOV
1826
            particle_group_buffer: AlignedBufferVec::new(
×
1827
                BufferUsages::STORAGE,
1828
                NonZeroU64::new(item_align),
1829
                Some("hanabi:buffer:particle_group".to_string()),
1830
            ),
1831
            default_mesh,
1832
            gpu_limits,
1833
        }
1834
    }
1835

1836
    /// Allocate internal resources for newly spawned effects, and deallocate
1837
    /// them for just-removed ones.
UNCOV
1838
    pub fn add_remove_effects(
×
1839
        &mut self,
1840
        mut added_effects: Vec<AddedEffect>,
1841
        removed_effect_entities: Vec<Entity>,
1842
        render_device: &RenderDevice,
1843
        render_queue: &RenderQueue,
1844
        effect_bind_groups: &mut ResMut<EffectBindGroups>,
1845
        effect_cache: &mut ResMut<EffectCache>,
1846
    ) {
1847
        // Deallocate GPU data for destroyed effect instances. This will automatically
1848
        // drop any group where there is no more effect slice.
UNCOV
1849
        trace!(
×
1850
            "Removing {} despawned effects",
×
1851
            removed_effect_entities.len()
×
1852
        );
UNCOV
1853
        for entity in &removed_effect_entities {
×
1854
            trace!("Removing ParticleEffect on entity {:?}", entity);
×
NEW
1855
            if let Some(effect_cache_id) = self.entity_map.remove(entity) {
×
1856
                trace!(
1857
                    "=> ParticleEffect on entity {:?} had cache ID {:?}, removing...",
×
1858
                    entity,
1859
                    effect_cache_id
1860
                );
NEW
1861
                if let Some(cached_effect) = effect_cache.remove(effect_cache_id) {
×
1862
                    // Clear bind groups associated with the removed buffer
1863
                    trace!(
1864
                        "=> GPU buffer #{} gone, destroying its bind groups...",
×
1865
                        cached_effect.buffer_index
1866
                    );
1867
                    effect_bind_groups
×
1868
                        .particle_buffers
×
NEW
1869
                        .remove(&cached_effect.buffer_index);
×
1870

NEW
1871
                    let slices_ref = &cached_effect.slices;
×
1872
                    debug_assert!(slices_ref.ranges.len() >= 2);
×
1873
                    let group_count = (slices_ref.ranges.len() - 1) as u32;
×
1874

1875
                    let first_row = slices_ref
×
1876
                        .dispatch_buffer_indices
×
1877
                        .first_update_group_dispatch_buffer_index
×
1878
                        .0;
×
1879
                    for table_id in first_row..(first_row + group_count) {
×
1880
                        self.dispatch_indirect_buffer
×
1881
                            .remove(BufferTableId(table_id));
×
1882
                    }
1883
                    self.render_effect_dispatch_buffer.remove(
×
1884
                        slices_ref
×
1885
                            .dispatch_buffer_indices
×
1886
                            .render_effect_metadata_buffer_index,
×
1887
                    );
1888
                    if let RenderGroupDispatchIndices::Allocated {
NEW
1889
                        first_render_group_dispatch_buffer_index,
×
1890
                        ..
NEW
1891
                    } = &slices_ref
×
1892
                        .dispatch_buffer_indices
×
NEW
1893
                        .render_group_dispatch_indices
×
1894
                    {
NEW
1895
                        for row_index in (first_render_group_dispatch_buffer_index.0)
×
1896
                            ..(first_render_group_dispatch_buffer_index.0 + group_count)
1897
                        {
NEW
1898
                            self.render_group_dispatch_buffer
×
NEW
1899
                                .remove(BufferTableId(row_index));
×
1900
                        }
1901
                    }
1902
                }
1903
            }
1904
        }
1905

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

UNCOV
1912
        trace!("Adding {} newly spawned effects", added_effects.len());
×
UNCOV
1913
        for added_effect in added_effects.drain(..) {
×
1914
            // Allocate per-group update dispatch indirect
1915
            let first_update_group_dispatch_buffer_index = allocate_sequential_buffers(
1916
                &mut self.dispatch_indirect_buffer,
×
1917
                iter::repeat(GpuDispatchIndirect::default()).take(added_effect.groups.len()),
×
1918
            );
1919

1920
            // Allocate per-effect metadata
1921
            let render_effect_dispatch_buffer_id = self
×
1922
                .render_effect_dispatch_buffer
×
1923
                .insert(GpuRenderEffectMetadata::default());
×
1924

1925
            let dispatch_buffer_indices = DispatchBufferIndices {
1926
                first_update_group_dispatch_buffer_index,
1927
                render_effect_metadata_buffer_index: render_effect_dispatch_buffer_id,
NEW
1928
                render_group_dispatch_indices: RenderGroupDispatchIndices::Pending {
×
1929
                    groups: added_effect.groups.iter().map(Into::into).collect(),
1930
                },
1931
            };
1932

1933
            // Insert the effect into the cache. This will allocate all the necessary GPU
1934
            // resources as needed.
1935
            let cache_id = effect_cache.insert(
×
1936
                added_effect.handle,
×
1937
                added_effect
×
1938
                    .groups
×
1939
                    .iter()
×
1940
                    .map(|group| group.capacity)
×
1941
                    .collect(),
×
1942
                &added_effect.particle_layout,
×
1943
                &added_effect.property_layout,
×
1944
                added_effect.layout_flags,
×
1945
                dispatch_buffer_indices,
×
1946
                added_effect.group_order,
×
1947
            );
1948

1949
            let entity = added_effect.entity;
×
NEW
1950
            self.entity_map.insert(entity, cache_id);
×
1951

NEW
1952
            trace!(
×
NEW
1953
                "+ added effect cache ID {:?}: entity={:?} \
×
NEW
1954
                first_update_group_dispatch_buffer_index={} \
×
NEW
1955
                render_effect_dispatch_buffer_id={}",
×
1956
                cache_id,
1957
                entity,
1958
                first_update_group_dispatch_buffer_index.0,
1959
                render_effect_dispatch_buffer_id.0
1960
            );
1961

1962
            // Note: those effects are already in extracted_effects.effects
1963
            // because they were gathered by the same query as
1964
            // previously existing ones, during extraction.
1965

1966
            // let index = self.effect_cache.buffer_index(cache_id).unwrap();
1967
            //
1968
            // let table_id = self
1969
            // .dispatch_indirect_buffer
1970
            // .insert(GpuDispatchIndirect::default());
1971
            // assert_eq!(
1972
            // table_id.0, index,
1973
            // "Broken table invariant: buffer={} row={}",
1974
            // index, table_id.0
1975
            // );
1976
        }
1977

1978
        // Once all changes are applied, immediately schedule any GPU buffer
1979
        // (re)allocation based on the new buffer size. The actual GPU buffer content
1980
        // will be written later.
UNCOV
1981
        if self
×
UNCOV
1982
            .dispatch_indirect_buffer
×
UNCOV
1983
            .allocate_gpu(render_device, render_queue)
×
1984
        {
1985
            // All those bind groups use the buffer so need to be re-created
1986
            effect_bind_groups.particle_buffers.clear();
×
1987
        }
UNCOV
1988
        if self
×
UNCOV
1989
            .render_effect_dispatch_buffer
×
UNCOV
1990
            .allocate_gpu(render_device, render_queue)
×
1991
        {
1992
            // All those bind groups use the buffer so need to be re-created
1993
            self.dr_indirect_bind_group = None;
×
1994
            self.init_render_indirect_spawn_bind_group = None;
×
1995
            self.init_render_indirect_clone_bind_group = None;
×
1996
            effect_bind_groups
×
1997
                .update_render_indirect_bind_groups
×
1998
                .clear();
1999
        }
2000
    }
2001

NEW
2002
    pub fn allocate_gpu(
×
2003
        &mut self,
2004
        render_device: &RenderDevice,
2005
        render_queue: &RenderQueue,
2006
        effect_bind_groups: &mut ResMut<EffectBindGroups>,
2007
    ) {
UNCOV
2008
        if self
×
UNCOV
2009
            .render_group_dispatch_buffer
×
UNCOV
2010
            .allocate_gpu(render_device, render_queue)
×
2011
        {
2012
            // All those bind groups use the buffer so need to be re-created
2013
            self.dr_indirect_bind_group = None;
×
2014
            self.init_render_indirect_spawn_bind_group = None;
×
2015
            self.init_render_indirect_clone_bind_group = None;
×
2016
            effect_bind_groups
×
2017
                .update_render_indirect_bind_groups
×
2018
                .clear();
2019
        }
2020
    }
2021
}
2022

2023
bitflags! {
2024
    /// Effect flags.
2025
    #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
2026
    pub struct LayoutFlags: u32 {
2027
        /// No flags.
2028
        const NONE = 0;
2029
        // DEPRECATED - The effect uses an image texture.
2030
        //const PARTICLE_TEXTURE = (1 << 0);
2031
        /// The effect is simulated in local space.
2032
        const LOCAL_SPACE_SIMULATION = (1 << 2);
2033
        /// The effect uses alpha masking instead of alpha blending. Only used for 3D.
2034
        const USE_ALPHA_MASK = (1 << 3);
2035
        /// The effect is rendered with flipbook texture animation based on the [`Attribute::SPRITE_INDEX`] of each particle.
2036
        const FLIPBOOK = (1 << 4);
2037
        /// The effect needs UVs.
2038
        const NEEDS_UV = (1 << 5);
2039
        /// The effect has ribbons.
2040
        const RIBBONS = (1 << 6);
2041
        /// The effects needs normals.
2042
        const NEEDS_NORMAL = (1 << 7);
2043
        /// The effect is fully-opaque.
2044
        const OPAQUE = (1 << 8);
2045
    }
2046
}
2047

2048
impl Default for LayoutFlags {
2049
    fn default() -> Self {
1✔
2050
        Self::NONE
1✔
2051
    }
2052
}
2053

UNCOV
2054
pub(crate) fn prepare_effects(
×
2055
    mut commands: Commands,
2056
    sim_params: Res<SimParams>,
2057
    render_device: Res<RenderDevice>,
2058
    render_queue: Res<RenderQueue>,
2059
    pipeline_cache: Res<PipelineCache>,
2060
    init_pipeline: Res<ParticlesInitPipeline>,
2061
    update_pipeline: Res<ParticlesUpdatePipeline>,
2062
    mesh_allocator: Res<MeshAllocator>,
2063
    render_meshes: Res<RenderAssets<RenderMesh>>,
2064
    mut specialized_init_pipelines: ResMut<SpecializedComputePipelines<ParticlesInitPipeline>>,
2065
    mut specialized_update_pipelines: ResMut<SpecializedComputePipelines<ParticlesUpdatePipeline>>,
2066
    mut effects_meta: ResMut<EffectsMeta>,
2067
    mut effect_cache: ResMut<EffectCache>,
2068
    mut extracted_effects: ResMut<ExtractedEffects>,
2069
    mut effect_bind_groups: ResMut<EffectBindGroups>,
2070
) {
UNCOV
2071
    trace!("prepare_effects");
×
2072

2073
    // Clear last frame's buffer resizes which may have occured during last frame,
2074
    // during `Node::run()` while the `BufferTable` could not be mutated. This is
2075
    // the first point at which we can do that where we're not blocking the main
2076
    // world (so, excluding the extract system).
UNCOV
2077
    effects_meta
×
UNCOV
2078
        .dispatch_indirect_buffer
×
2079
        .clear_previous_frame_resizes();
UNCOV
2080
    effects_meta
×
UNCOV
2081
        .render_effect_dispatch_buffer
×
2082
        .clear_previous_frame_resizes();
UNCOV
2083
    effects_meta
×
UNCOV
2084
        .render_group_dispatch_buffer
×
2085
        .clear_previous_frame_resizes();
2086

2087
    // Allocate new effects, deallocate removed ones
UNCOV
2088
    let removed_effect_entities = std::mem::take(&mut extracted_effects.removed_effect_entities);
×
UNCOV
2089
    for entity in &removed_effect_entities {
×
2090
        extracted_effects.effects.remove(entity);
×
2091
    }
2092
    effects_meta.add_remove_effects(
2093
        std::mem::take(&mut extracted_effects.added_effects),
2094
        removed_effect_entities,
2095
        &render_device,
2096
        &render_queue,
2097
        &mut effect_bind_groups,
2098
        &mut effect_cache,
2099
    );
2100

2101
    // // sort first by z and then by handle. this ensures that, when possible,
2102
    // batches span multiple z layers // batches won't span z-layers if there is
2103
    // another batch between them extracted_effects.effects.sort_by(|a, b| {
2104
    //     match FloatOrd(a.transform.w_axis[2]).cmp(&FloatOrd(b.transform.
2105
    // w_axis[2])) {         Ordering::Equal => a.handle.cmp(&b.handle),
2106
    //         other => other,
2107
    //     }
2108
    // });
2109

2110
    // Build batcher inputs from extracted effects
2111
    let effects = std::mem::take(&mut extracted_effects.effects);
2112
    let effect_entity_list = effects
2113
        .into_iter()
NEW
2114
        .filter_map(|(entity, extracted_effect)| {
×
NEW
2115
            let effect_cache_id = *effects_meta.entity_map.get(&entity).unwrap();
×
2116

2117
            // If the mesh is not available, skip this effect
NEW
2118
            let Some(render_mesh) = render_meshes.get(extracted_effect.mesh.id()) else {
×
NEW
2119
                trace!(
×
NEW
2120
                    "Effect cache ID {:?}: missing render mesh {:?}",
×
2121
                    effect_cache_id,
2122
                    extracted_effect.mesh
2123
                );
NEW
2124
                return None;
×
2125
            };
NEW
2126
            let Some(mesh_vertex_buffer_slice) =
×
2127
                mesh_allocator.mesh_vertex_slice(&extracted_effect.mesh.id())
2128
            else {
NEW
2129
                trace!(
×
NEW
2130
                    "Effect cache ID {:?}: missing render mesh vertex slice",
×
2131
                    effect_cache_id
2132
                );
NEW
2133
                return None;
×
2134
            };
2135
            let mesh_index_buffer_slice =
2136
                mesh_allocator.mesh_index_slice(&extracted_effect.mesh.id());
NEW
2137
            if matches!(
×
2138
                render_mesh.buffer_info,
2139
                RenderMeshBufferInfo::Indexed { .. }
NEW
2140
            ) && mesh_index_buffer_slice.is_none()
×
2141
            {
NEW
2142
                trace!(
×
NEW
2143
                    "Effect cache ID {:?}: missing render mesh index slice",
×
2144
                    effect_cache_id
2145
                );
NEW
2146
                return None;
×
2147
            }
2148

2149
            // Now that the mesh has been processed by Bevy itself, we know where it's
2150
            // allocated inside the GPU buffer, so we can extract its base vertex and index
2151
            // values, and allocate our indirect structs.
NEW
2152
            let dispatch_buffer_indices =
×
NEW
2153
                effect_cache.get_dispatch_buffer_indices_mut(effect_cache_id);
×
NEW
2154
            if let RenderGroupDispatchIndices::Pending { groups } =
×
2155
                &dispatch_buffer_indices.render_group_dispatch_indices
2156
            {
NEW
2157
                trace!("Effect cache ID {:?}: allocating render group indirect dispatch entries for {} groups...", effect_cache_id, groups.len());
×
NEW
2158
                let mut current_base_instance = 0;
×
NEW
2159
                let first_render_group_dispatch_buffer_index = allocate_sequential_buffers(
×
NEW
2160
                    &mut effects_meta.render_group_dispatch_buffer,
×
NEW
2161
                    groups.iter().map(|group| {
×
NEW
2162
                        let indirect_dispatch = match &mesh_index_buffer_slice {
×
2163
                            // Indexed mesh rendering
NEW
2164
                            Some(mesh_index_buffer_slice) => {
×
NEW
2165
                                let ret = GpuRenderGroupIndirect {
×
NEW
2166
                                    vertex_count: mesh_index_buffer_slice.range.len() as u32,
×
NEW
2167
                                    instance_count: 0,
×
NEW
2168
                                    first_index_or_vertex_offset: mesh_index_buffer_slice.range.start,
×
NEW
2169
                                    vertex_offset_or_base_instance: mesh_vertex_buffer_slice.range.start as i32,
×
NEW
2170
                                    base_instance: current_base_instance as u32,
×
NEW
2171
                                    alive_count: 0,
×
NEW
2172
                                    max_update: 0,
×
NEW
2173
                                    dead_count: group.capacity,
×
NEW
2174
                                    max_spawn: group.capacity,
×
2175
                                };
NEW
2176
                                trace!("+ Group[indexed]: {:?}", ret);
×
NEW
2177
                                ret
×
2178
                            },
2179
                            // Non-indexed mesh rendering
2180
                            None => {
NEW
2181
                                let ret = GpuRenderGroupIndirect {
×
NEW
2182
                                    vertex_count: mesh_vertex_buffer_slice.range.len() as u32,
×
NEW
2183
                                    instance_count: 0,
×
NEW
2184
                                    first_index_or_vertex_offset: mesh_vertex_buffer_slice.range.start,
×
NEW
2185
                                    vertex_offset_or_base_instance: current_base_instance,
×
NEW
2186
                                    base_instance: current_base_instance as u32,
×
NEW
2187
                                    alive_count: 0,
×
NEW
2188
                                    max_update: 0,
×
NEW
2189
                                    dead_count: group.capacity,
×
NEW
2190
                                    max_spawn: group.capacity,
×
2191
                                };
NEW
2192
                                trace!("+ Group[non-indexed]: {:?}", ret);
×
NEW
2193
                                ret
×
2194
                            },
2195
                        };
2196
                        current_base_instance += group.capacity as i32;
2197
                        indirect_dispatch
2198
                    }),
2199
                );
2200

2201
                let mut trail_dispatch_buffer_indices = HashMap::new();
NEW
2202
                for (dest_group_index, group) in groups.iter().enumerate() {
×
NEW
2203
                    let Some(src_group_index) = group.src_group_index_if_trail else {
×
NEW
2204
                        continue;
×
2205
                    };
2206
                    trail_dispatch_buffer_indices.insert(
2207
                        dest_group_index as u32,
2208
                        TrailDispatchBufferIndices {
2209
                            dest: first_render_group_dispatch_buffer_index
2210
                                .offset(dest_group_index as u32),
2211
                            src: first_render_group_dispatch_buffer_index.offset(src_group_index),
2212
                        },
2213
                    );
2214
                }
2215

NEW
2216
                trace!(
×
NEW
2217
                    "-> Allocated {} render group dispatch indirect entries at offset +{}. Trails: {:?}",
×
NEW
2218
                    groups.len(),
×
2219
                    first_render_group_dispatch_buffer_index.0,
2220
                    trail_dispatch_buffer_indices
2221
                );
NEW
2222
                dispatch_buffer_indices.render_group_dispatch_indices =
×
NEW
2223
                    RenderGroupDispatchIndices::Allocated {
×
NEW
2224
                        first_render_group_dispatch_buffer_index,
×
NEW
2225
                        trail_dispatch_buffer_indices,
×
2226
                    };
2227
            }
2228

NEW
2229
            let property_buffer = effect_cache.get_property_buffer(effect_cache_id).cloned(); // clone handle for lifetime
×
NEW
2230
            let effect_slices = effect_cache.get_slices(effect_cache_id);
×
NEW
2231
            let group_order = effect_cache.get_group_order(effect_cache_id);
×
2232

NEW
2233
            Some(BatchesInput {
×
2234
                handle: extracted_effect.handle,
×
2235
                entity,
×
2236
                effect_slices,
×
2237
                property_layout: extracted_effect.property_layout.clone(),
×
2238
                effect_shaders: extracted_effect.effect_shaders.clone(),
×
2239
                layout_flags: extracted_effect.layout_flags,
×
2240
                mesh: extracted_effect.mesh,
×
NEW
2241
                mesh_buffer: mesh_vertex_buffer_slice.buffer.clone(),
×
NEW
2242
                mesh_slice: mesh_vertex_buffer_slice.range.clone(),
×
2243
                texture_layout: extracted_effect.texture_layout.clone(),
×
2244
                textures: extracted_effect.textures.clone(),
×
2245
                alpha_mode: extracted_effect.alpha_mode,
×
2246
                transform: extracted_effect.transform.into(),
×
2247
                inverse_transform: extracted_effect.inverse_transform.into(),
×
2248
                particle_layout: extracted_effect.particle_layout.clone(),
×
2249
                property_buffer,
×
2250
                group_order: group_order.to_vec(),
×
2251
                property_data: extracted_effect.property_data,
×
2252
                initializers: extracted_effect.initializers,
×
2253
                #[cfg(feature = "2d")]
×
2254
                z_sort_key_2d: extracted_effect.z_sort_key_2d,
×
2255
            })
2256
        })
2257
        .collect::<Vec<_>>();
NEW
2258
    trace!("Collected {} extracted effect(s)", effect_entity_list.len());
×
2259

2260
    // Perform any GPU allocation if we (lazily) allocated some rows into the render
2261
    // group dispatch indirect buffer.
NEW
2262
    effects_meta.allocate_gpu(&render_device, &render_queue, &mut effect_bind_groups);
×
2263

2264
    // Sort first by effect buffer index, then by slice range (see EffectSlice)
2265
    // inside that buffer. This is critical for batching to work, because
2266
    // batching effects is based on compatible items, which implies same GPU
2267
    // buffer and continuous slice ranges (the next slice start must be equal to
2268
    // the previous start end, without gap). EffectSlice already contains both
2269
    // information, and the proper ordering implementation.
2270
    // effect_entity_list.sort_by_key(|a| a.effect_slice.clone());
2271

2272
    // Loop on all extracted effects in order, and try to batch them together to
2273
    // reduce draw calls.
UNCOV
2274
    effects_meta.spawner_buffer.clear();
×
UNCOV
2275
    effects_meta.particle_group_buffer.clear();
×
UNCOV
2276
    let mut total_group_count = 0;
×
NEW
2277
    for input in effect_entity_list.into_iter() {
×
2278
        let particle_layout_min_binding_size =
×
2279
            input.effect_slices.particle_layout.min_binding_size();
×
2280
        let property_layout_min_binding_size = if input.property_layout.is_empty() {
×
2281
            None
×
2282
        } else {
2283
            Some(input.property_layout.min_binding_size())
×
2284
        };
2285

2286
        // Create init pipeline key flags.
2287
        let mut init_pipeline_key_flags = ParticleInitPipelineKeyFlags::empty();
×
2288
        init_pipeline_key_flags.set(
×
2289
            ParticleInitPipelineKeyFlags::ATTRIBUTE_PREV,
×
2290
            input.particle_layout.contains(Attribute::PREV),
×
2291
        );
2292
        init_pipeline_key_flags.set(
×
2293
            ParticleInitPipelineKeyFlags::ATTRIBUTE_NEXT,
×
2294
            input.particle_layout.contains(Attribute::NEXT),
×
2295
        );
2296

2297
        // Specialize the init pipeline based on the effect.
2298
        let init_and_update_pipeline_ids = input
×
2299
            .effect_shaders
×
2300
            .iter()
2301
            .enumerate()
2302
            .map(|(group_index, shader)| {
×
2303
                let mut flags = init_pipeline_key_flags;
×
2304

2305
                // If this is a cloner, add the appropriate flag.
2306
                match input.initializers[group_index] {
×
2307
                    EffectInitializer::Spawner(_) => {}
×
2308
                    EffectInitializer::Cloner(_) => {
×
2309
                        flags.insert(ParticleInitPipelineKeyFlags::CLONE);
×
2310
                    }
2311
                }
2312

2313
                let init_pipeline_id = specialized_init_pipelines.specialize(
×
2314
                    &pipeline_cache,
×
2315
                    &init_pipeline,
×
2316
                    ParticleInitPipelineKey {
×
2317
                        shader: shader.init.clone(),
×
2318
                        particle_layout_min_binding_size,
×
2319
                        property_layout_min_binding_size,
×
2320
                        flags,
×
2321
                    },
2322
                );
2323
                trace!("Init pipeline specialized: id={:?}", init_pipeline_id);
×
2324

2325
                let update_pipeline_id = specialized_update_pipelines.specialize(
×
2326
                    &pipeline_cache,
2327
                    &update_pipeline,
2328
                    ParticleUpdatePipelineKey {
2329
                        shader: shader.update.clone(),
2330
                        particle_layout: input.effect_slices.particle_layout.clone(),
2331
                        property_layout: input.property_layout.clone(),
2332
                        is_trail: matches!(
×
2333
                            input.initializers[group_index],
2334
                            EffectInitializer::Cloner(_)
2335
                        ),
2336
                    },
2337
                );
2338
                trace!("Update pipeline specialized: id={:?}", update_pipeline_id);
×
2339

2340
                InitAndUpdatePipelineIds {
×
2341
                    init: init_pipeline_id,
×
2342
                    update: update_pipeline_id,
×
2343
                }
2344
            })
2345
            .collect();
2346

2347
        let init_shaders: Vec<_> = input
×
2348
            .effect_shaders
×
2349
            .iter()
2350
            .map(|shaders| shaders.init.clone())
×
2351
            .collect();
2352
        trace!("init_shader(s) = {:?}", init_shaders);
×
2353

2354
        let update_shaders: Vec<_> = input
×
2355
            .effect_shaders
×
2356
            .iter()
2357
            .map(|shaders| shaders.update.clone())
×
2358
            .collect();
2359
        trace!("update_shader(s) = {:?}", update_shaders);
×
2360

2361
        let render_shaders: Vec<_> = input
×
2362
            .effect_shaders
×
2363
            .iter()
2364
            .map(|shaders| shaders.render.clone())
×
2365
            .collect();
2366
        trace!("render_shader(s) = {:?}", render_shaders);
×
2367

2368
        let layout_flags = input.layout_flags;
×
2369
        trace!("layout_flags = {:?}", layout_flags);
×
2370

2371
        trace!(
×
2372
            "particle_layout = {:?}",
×
2373
            input.effect_slices.particle_layout
2374
        );
2375

2376
        #[cfg(feature = "2d")]
2377
        {
2378
            trace!("z_sort_key_2d = {:?}", input.z_sort_key_2d);
×
2379
        }
2380

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

2386
        for initializer in input.initializers.iter() {
×
2387
            match initializer {
×
2388
                EffectInitializer::Spawner(effect_spawner) => {
×
2389
                    let spawner_params = GpuSpawnerParams {
2390
                        transform: input.transform,
×
2391
                        inverse_transform: input.inverse_transform,
×
2392
                        spawn: effect_spawner.spawn_count as i32,
×
2393
                        // FIXME - Probably bad to re-seed each time there's a change
2394
                        seed: random::<u32>(),
×
2395
                        count: 0,
2396
                        // FIXME: the effect_index is global inside the global spawner buffer,
2397
                        // but the group_index is the index of the particle buffer, which can
2398
                        // in theory (with batching) contain > 1 effect per buffer.
2399
                        effect_index: input.effect_slices.buffer_index,
×
2400
                        lifetime: 0.0,
2401
                        pad: Default::default(),
×
2402
                    };
2403
                    trace!("spawner params = {:?}", spawner_params);
×
2404
                    effects_meta.spawner_buffer.push(spawner_params);
×
2405
                }
2406

2407
                EffectInitializer::Cloner(effect_cloner) => {
×
2408
                    let spawner_params = GpuSpawnerParams {
2409
                        transform: input.transform,
×
2410
                        inverse_transform: input.inverse_transform,
×
2411
                        spawn: 0,
2412
                        // FIXME - Probably bad to re-seed each time there's a change
2413
                        seed: random::<u32>(),
×
2414
                        count: 0,
2415
                        // FIXME: the effect_index is global inside the global spawner buffer,
2416
                        // but the group_index is the index of the particle buffer, which can
2417
                        // in theory (with batching) contain > 1 effect per buffer.
2418
                        effect_index: input.effect_slices.buffer_index,
×
2419
                        lifetime: effect_cloner.cloner.lifetime,
×
2420
                        pad: Default::default(),
×
2421
                    };
2422
                    trace!("cloner params = {:?}", spawner_params);
×
2423
                    effects_meta.spawner_buffer.push(spawner_params);
×
2424
                }
2425
            }
2426
        }
2427

NEW
2428
        let effect_cache_id = *effects_meta.entity_map.get(&input.entity).unwrap();
×
NEW
2429
        let dispatch_buffer_indices = effect_cache
×
NEW
2430
            .get_dispatch_buffer_indices(effect_cache_id)
×
2431
            .clone();
2432

2433
        // Create the particle group buffer entries.
2434
        let mut first_particle_group_buffer_index = None;
×
2435
        let mut local_group_count = 0;
×
2436
        for (group_index, range) in input.effect_slices.slices.windows(2).enumerate() {
×
2437
            let particle_group_buffer_index =
×
2438
                effects_meta.particle_group_buffer.push(GpuParticleGroup {
×
2439
                    global_group_index: total_group_count,
×
NEW
2440
                    effect_index: dispatch_buffer_indices
×
NEW
2441
                        .render_effect_metadata_buffer_index
×
NEW
2442
                        .0,
×
2443
                    group_index_in_effect: group_index as u32,
×
2444
                    indirect_index: range[0],
×
2445
                    capacity: range[1] - range[0],
×
2446
                    effect_particle_offset: input.effect_slices.slices[0],
×
NEW
2447
                    indirect_dispatch_index: dispatch_buffer_indices
×
NEW
2448
                        .first_update_group_dispatch_buffer_index
×
NEW
2449
                        .0
×
NEW
2450
                        + group_index as u32,
×
2451
                });
2452
            if group_index == 0 {
×
2453
                first_particle_group_buffer_index = Some(particle_group_buffer_index as u32);
×
2454
            }
2455
            total_group_count += 1;
×
2456
            local_group_count += 1;
×
2457
        }
2458

2459
        // Write properties for this effect if they were modified.
2460
        // FIXME - This doesn't work with batching!
2461
        if let Some(property_data) = &input.property_data {
×
2462
            trace!("Properties changed, need to (re-)upload to GPU");
×
2463
            if let Some(property_buffer) = input.property_buffer.as_ref() {
×
2464
                trace!("Scheduled property upload to GPU");
×
2465
                render_queue.write_buffer(property_buffer, 0, property_data);
×
2466
            } else {
2467
                error!("Cannot upload properties to GPU, no property buffer!");
×
2468
            }
2469
        }
2470

2471
        #[cfg(feature = "2d")]
2472
        let z_sort_key_2d = input.z_sort_key_2d;
×
2473

2474
        #[cfg(feature = "3d")]
2475
        let translation_3d = input.transform.translation();
×
2476

2477
        // Spawn one shared EffectBatches for all groups of this effect. This contains
2478
        // most of the data needed to drive rendering, except the per-group data.
2479
        // However this doesn't drive rendering; this is just storage.
2480
        let batches = EffectBatches::from_input(
2481
            input,
×
2482
            spawner_base,
×
2483
            effect_cache_id,
×
2484
            init_and_update_pipeline_ids,
×
2485
            dispatch_buffer_indices,
×
2486
            first_particle_group_buffer_index.unwrap_or_default(),
×
2487
        );
NEW
2488
        let batches_entity = commands.spawn(batches).insert(TemporaryRenderEntity).id();
×
2489

2490
        // Spawn one EffectDrawBatch per group, to actually drive rendering. Each group
2491
        // renders with a different indirect call. These are the entities that the
2492
        // render phase items will receive.
2493
        for group_index in 0..local_group_count {
×
NEW
2494
            commands
×
NEW
2495
                .spawn(EffectDrawBatch {
×
NEW
2496
                    batches_entity,
×
NEW
2497
                    group_index,
×
NEW
2498
                    #[cfg(feature = "2d")]
×
NEW
2499
                    z_sort_key_2d,
×
NEW
2500
                    #[cfg(feature = "3d")]
×
NEW
2501
                    translation_3d,
×
2502
                })
NEW
2503
                .insert(TemporaryRenderEntity);
×
2504
        }
2505
    }
2506

2507
    // Write the entire spawner buffer for this frame, for all effects combined
UNCOV
2508
    effects_meta
×
UNCOV
2509
        .spawner_buffer
×
UNCOV
2510
        .write_buffer(&render_device, &render_queue);
×
2511

2512
    // Write the entire particle group buffer for this frame
UNCOV
2513
    if effects_meta
×
UNCOV
2514
        .particle_group_buffer
×
UNCOV
2515
        .write_buffer(&render_device, &render_queue)
×
2516
    {
2517
        // The buffer changed; invalidate all bind groups for all effects.
2518
    }
2519

2520
    // Update simulation parameters
UNCOV
2521
    effects_meta
×
UNCOV
2522
        .sim_params_uniforms
×
UNCOV
2523
        .set(sim_params.deref().into());
×
2524
    {
UNCOV
2525
        let gpu_sim_params = effects_meta.sim_params_uniforms.get_mut();
×
UNCOV
2526
        gpu_sim_params.num_groups = total_group_count;
×
2527

UNCOV
2528
        trace!(
×
2529
            "Simulation parameters: time={} delta_time={} virtual_time={} \
×
2530
                virtual_delta_time={} real_time={} real_delta_time={} num_groups={}",
×
2531
            gpu_sim_params.time,
2532
            gpu_sim_params.delta_time,
2533
            gpu_sim_params.virtual_time,
2534
            gpu_sim_params.virtual_delta_time,
2535
            gpu_sim_params.real_time,
2536
            gpu_sim_params.real_delta_time,
2537
            gpu_sim_params.num_groups,
2538
        );
2539
    }
UNCOV
2540
    let prev_buffer_id = effects_meta.sim_params_uniforms.buffer().map(|b| b.id());
×
2541
    effects_meta
2542
        .sim_params_uniforms
2543
        .write_buffer(&render_device, &render_queue);
UNCOV
2544
    if prev_buffer_id != effects_meta.sim_params_uniforms.buffer().map(|b| b.id()) {
×
2545
        // Buffer changed, invalidate bind groups
UNCOV
2546
        effects_meta.sim_params_bind_group = None;
×
2547
    }
2548
}
2549

2550
/// Per-buffer bind groups for a GPU effect buffer.
2551
///
2552
/// This contains all bind groups specific to a single [`EffectBuffer`].
2553
///
2554
/// [`EffectBuffer`]: crate::render::effect_cache::EffectBuffer
2555
pub(crate) struct BufferBindGroups {
2556
    /// Bind group for the render graphic shader.
2557
    ///
2558
    /// ```wgsl
2559
    /// @binding(0) var<storage, read> particle_buffer : ParticleBuffer;
2560
    /// @binding(1) var<storage, read> indirect_buffer : IndirectBuffer;
2561
    /// @binding(2) var<storage, read> dispatch_indirect : DispatchIndirect;
2562
    /// #ifdef RENDER_NEEDS_SPAWNER
2563
    /// @binding(3) var<storage, read> spawner : Spawner;
2564
    /// #endif
2565
    /// ```
2566
    render: BindGroup,
2567
}
2568

2569
/// Combination of a texture layout and the bound textures.
2570
#[derive(Debug, Default, Clone, PartialEq, Eq, Hash)]
2571
struct Material {
2572
    layout: TextureLayout,
2573
    textures: Vec<AssetId<Image>>,
2574
}
2575

2576
impl Material {
2577
    /// Get the bind group entries to create a bind group.
2578
    pub fn make_entries<'a>(
×
2579
        &self,
2580
        gpu_images: &'a RenderAssets<GpuImage>,
2581
    ) -> Result<Vec<BindGroupEntry<'a>>, ()> {
2582
        if self.textures.is_empty() {
×
2583
            return Ok(vec![]);
×
2584
        }
2585

2586
        let entries: Vec<BindGroupEntry<'a>> = self
×
2587
            .textures
×
2588
            .iter()
2589
            .enumerate()
2590
            .flat_map(|(index, id)| {
×
2591
                let base_binding = index as u32 * 2;
×
2592
                if let Some(gpu_image) = gpu_images.get(*id) {
×
2593
                    vec![
×
2594
                        BindGroupEntry {
×
2595
                            binding: base_binding,
×
2596
                            resource: BindingResource::TextureView(&gpu_image.texture_view),
×
2597
                        },
2598
                        BindGroupEntry {
×
2599
                            binding: base_binding + 1,
×
2600
                            resource: BindingResource::Sampler(&gpu_image.sampler),
×
2601
                        },
2602
                    ]
2603
                } else {
2604
                    vec![]
×
2605
                }
2606
            })
2607
            .collect();
2608
        if entries.len() == self.textures.len() * 2 {
×
2609
            return Ok(entries);
×
2610
        }
2611
        Err(())
×
2612
    }
2613
}
2614

2615
#[derive(Default, Resource)]
2616
pub struct EffectBindGroups {
2617
    /// Map from buffer index to the bind groups shared among all effects that
2618
    /// use that buffer.
2619
    particle_buffers: HashMap<u32, BufferBindGroups>,
2620
    /// Map of bind groups for image assets used as particle textures.
2621
    images: HashMap<AssetId<Image>, BindGroup>,
2622
    /// Map from effect index to its update render indirect bind group (group
2623
    /// 3).
2624
    update_render_indirect_bind_groups: HashMap<EffectCacheId, BindGroup>,
2625
    /// Map from an effect material to its bind group.
2626
    material_bind_groups: HashMap<Material, BindGroup>,
2627
}
2628

2629
impl EffectBindGroups {
2630
    pub fn particle_render(&self, buffer_index: u32) -> Option<&BindGroup> {
×
2631
        self.particle_buffers
×
2632
            .get(&buffer_index)
×
2633
            .map(|bg| &bg.render)
×
2634
    }
2635
}
2636

2637
#[derive(SystemParam)]
2638
pub struct QueueEffectsReadOnlyParams<'w, 's> {
2639
    #[cfg(feature = "2d")]
2640
    draw_functions_2d: Res<'w, DrawFunctions<Transparent2d>>,
2641
    #[cfg(feature = "3d")]
2642
    draw_functions_3d: Res<'w, DrawFunctions<Transparent3d>>,
2643
    #[cfg(feature = "3d")]
2644
    draw_functions_alpha_mask: Res<'w, DrawFunctions<AlphaMask3d>>,
2645
    #[cfg(feature = "3d")]
2646
    draw_functions_opaque: Res<'w, DrawFunctions<Opaque3d>>,
2647
    #[system_param(ignore)]
2648
    marker: PhantomData<&'s usize>,
2649
}
2650

2651
fn emit_sorted_draw<T, F>(
×
2652
    views: &Query<(Entity, &RenderVisibleEntities, &ExtractedView, &Msaa)>,
2653
    render_phases: &mut ResMut<ViewSortedRenderPhases<T>>,
2654
    view_entities: &mut FixedBitSet,
2655
    effect_batches: &Query<(Entity, &mut EffectBatches)>,
2656
    effect_draw_batches: &Query<(Entity, &mut EffectDrawBatch)>,
2657
    render_pipeline: &mut ParticlesRenderPipeline,
2658
    mut specialized_render_pipelines: Mut<SpecializedRenderPipelines<ParticlesRenderPipeline>>,
2659
    render_meshes: &RenderAssets<RenderMesh>,
2660
    pipeline_cache: &PipelineCache,
2661
    make_phase_item: F,
2662
    #[cfg(all(feature = "2d", feature = "3d"))] pipeline_mode: PipelineMode,
2663
) where
2664
    T: SortedPhaseItem,
2665
    F: Fn(CachedRenderPipelineId, (Entity, MainEntity), &EffectDrawBatch, u32, &ExtractedView) -> T,
2666
{
2667
    trace!("emit_sorted_draw() {} views", views.iter().len());
×
2668

NEW
2669
    for (view_entity, visible_entities, view, msaa) in views.iter() {
×
NEW
2670
        trace!(
×
NEW
2671
            "Process new sorted view with {} visible particle effect entities",
×
NEW
2672
            visible_entities.len::<WithCompiledParticleEffect>()
×
2673
        );
2674

2675
        let Some(render_phase) = render_phases.get_mut(&view_entity) else {
×
2676
            continue;
×
2677
        };
2678

2679
        {
2680
            #[cfg(feature = "trace")]
2681
            let _span = bevy::utils::tracing::info_span!("collect_view_entities").entered();
×
2682

2683
            view_entities.clear();
×
2684
            view_entities.extend(
×
2685
                visible_entities
×
2686
                    .iter::<WithCompiledParticleEffect>()
×
NEW
2687
                    .map(|e| e.1.index() as usize),
×
2688
            );
2689
        }
2690

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

2698
            trace!(
×
2699
                "Process draw batch: draw_entity={:?} group_index={} batches_entity={:?}",
×
2700
                draw_entity,
×
2701
                draw_batch.group_index,
×
2702
                draw_batch.batches_entity,
×
2703
            );
2704

2705
            // Get the EffectBatches this EffectDrawBatch is part of.
2706
            let Ok((batches_entity, batches)) = effect_batches.get(draw_batch.batches_entity)
×
2707
            else {
×
2708
                continue;
×
2709
            };
2710

2711
            trace!(
×
2712
                "-> EffectBaches: entity={:?} buffer_index={} spawner_base={} layout_flags={:?}",
×
2713
                batches_entity,
×
2714
                batches.buffer_index,
×
2715
                batches.spawner_base,
×
2716
                batches.layout_flags,
×
2717
            );
2718

2719
            // AlphaMask is a binned draw, so no sorted draw can possibly use it
2720
            if batches
×
2721
                .layout_flags
×
2722
                .intersects(LayoutFlags::USE_ALPHA_MASK | LayoutFlags::OPAQUE)
×
2723
            {
NEW
2724
                trace!("Non-transparent batch. Skipped.");
×
UNCOV
2725
                continue;
×
2726
            }
2727

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

2747
            // Create and cache the bind group layout for this texture layout
2748
            render_pipeline.cache_material(&batches.texture_layout);
×
2749

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

2753
            let local_space_simulation = batches
×
2754
                .layout_flags
×
2755
                .contains(LayoutFlags::LOCAL_SPACE_SIMULATION);
×
2756
            let alpha_mask = ParticleRenderAlphaMaskPipelineKey::from(batches.layout_flags);
×
2757
            let flipbook = batches.layout_flags.contains(LayoutFlags::FLIPBOOK);
×
2758
            let needs_uv = batches.layout_flags.contains(LayoutFlags::NEEDS_UV);
×
2759
            let needs_normal = batches.layout_flags.contains(LayoutFlags::NEEDS_NORMAL);
×
2760
            let ribbons = batches.layout_flags.contains(LayoutFlags::RIBBONS);
×
2761
            let image_count = batches.texture_layout.layout.len() as u8;
×
2762

2763
            // FIXME - Maybe it's better to copy the mesh layout into the batch, instead of
2764
            // re-querying here...?
NEW
2765
            let Some(render_mesh) = render_meshes.get(&batches.mesh) else {
×
NEW
2766
                trace!("Batch has no render mesh, skipped.");
×
NEW
2767
                continue;
×
2768
            };
NEW
2769
            let mesh_layout = render_mesh.layout.clone();
×
2770

2771
            // Specialize the render pipeline based on the effect batch
2772
            trace!(
×
2773
                "Specializing render pipeline: render_shaders={:?} image_count={} alpha_mask={:?} flipbook={:?} hdr={}",
×
2774
                batches.render_shaders,
×
2775
                image_count,
×
2776
                alpha_mask,
×
2777
                flipbook,
×
2778
                view.hdr
×
2779
            );
2780

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

2786
            let alpha_mode = batches.alpha_mode;
×
2787

2788
            #[cfg(feature = "trace")]
2789
            let _span_specialize = bevy::utils::tracing::info_span!("specialize").entered();
×
2790
            let render_pipeline_id = specialized_render_pipelines.specialize(
×
2791
                pipeline_cache,
×
2792
                render_pipeline,
×
2793
                ParticleRenderPipelineKey {
×
2794
                    shader: render_shader_source.clone(),
×
2795
                    mesh_layout: Some(mesh_layout),
×
2796
                    particle_layout: batches.particle_layout.clone(),
×
2797
                    texture_layout: batches.texture_layout.clone(),
×
2798
                    local_space_simulation,
×
2799
                    alpha_mask,
×
2800
                    alpha_mode,
×
2801
                    flipbook,
×
2802
                    needs_uv,
×
2803
                    needs_normal,
×
2804
                    ribbons,
×
2805
                    #[cfg(all(feature = "2d", feature = "3d"))]
×
2806
                    pipeline_mode,
×
NEW
2807
                    msaa_samples: msaa.samples(),
×
2808
                    hdr: view.hdr,
×
2809
                },
2810
            );
2811
            #[cfg(feature = "trace")]
2812
            _span_specialize.exit();
×
2813

2814
            trace!(
×
2815
                "+ Render pipeline specialized: id={:?} -> group_index={}",
×
2816
                render_pipeline_id,
×
2817
                draw_batch.group_index
×
2818
            );
2819
            trace!(
×
2820
                "+ Add Transparent for batch on draw_entity {:?}: buffer_index={} \
×
2821
                group_index={} spawner_base={} handle={:?}",
×
2822
                draw_entity,
×
2823
                batches.buffer_index,
×
2824
                draw_batch.group_index,
×
2825
                batches.spawner_base,
×
2826
                batches.handle
×
2827
            );
2828
            render_phase.add(make_phase_item(
×
2829
                render_pipeline_id,
×
NEW
2830
                (draw_entity, MainEntity::from(Entity::PLACEHOLDER)),
×
2831
                draw_batch,
×
2832
                draw_batch.group_index,
×
2833
                view,
×
2834
            ));
2835
        }
2836
    }
2837
}
2838

2839
#[cfg(feature = "3d")]
2840
fn emit_binned_draw<T, F>(
×
2841
    views: &Query<(Entity, &RenderVisibleEntities, &ExtractedView, &Msaa)>,
2842
    render_phases: &mut ResMut<ViewBinnedRenderPhases<T>>,
2843
    view_entities: &mut FixedBitSet,
2844
    effect_batches: &Query<(Entity, &mut EffectBatches)>,
2845
    effect_draw_batches: &Query<(Entity, &mut EffectDrawBatch)>,
2846
    render_pipeline: &mut ParticlesRenderPipeline,
2847
    mut specialized_render_pipelines: Mut<SpecializedRenderPipelines<ParticlesRenderPipeline>>,
2848
    pipeline_cache: &PipelineCache,
2849
    render_meshes: &RenderAssets<RenderMesh>,
2850
    make_bin_key: F,
2851
    #[cfg(all(feature = "2d", feature = "3d"))] pipeline_mode: PipelineMode,
2852
    alpha_mask: ParticleRenderAlphaMaskPipelineKey,
2853
) where
2854
    T: BinnedPhaseItem,
2855
    F: Fn(CachedRenderPipelineId, &EffectDrawBatch, u32, &ExtractedView) -> T::BinKey,
2856
{
2857
    use bevy::render::render_phase::BinnedRenderPhaseType;
2858

2859
    trace!("emit_binned_draw() {} views", views.iter().len());
×
2860

NEW
2861
    for (view_entity, visible_entities, view, msaa) in views.iter() {
×
2862
        trace!("Process new binned view (alpha_mask={:?})", alpha_mask);
×
2863

2864
        let Some(render_phase) = render_phases.get_mut(&view_entity) else {
×
2865
            continue;
×
2866
        };
2867

2868
        {
2869
            #[cfg(feature = "trace")]
2870
            let _span = bevy::utils::tracing::info_span!("collect_view_entities").entered();
×
2871

2872
            view_entities.clear();
×
2873
            view_entities.extend(
×
2874
                visible_entities
×
2875
                    .iter::<WithCompiledParticleEffect>()
×
NEW
2876
                    .map(|e| e.1.index() as usize),
×
2877
            );
2878
        }
2879

2880
        // For each view, loop over all the effect batches to determine if the effect
2881
        // needs to be rendered for that view, and enqueue a view-dependent
2882
        // batch if so.
2883
        for (draw_entity, draw_batch) in effect_draw_batches.iter() {
×
2884
            #[cfg(feature = "trace")]
2885
            let _span_draw = bevy::utils::tracing::info_span!("draw_batch").entered();
×
2886

2887
            trace!(
×
2888
                "Process draw batch: draw_entity={:?} group_index={} batches_entity={:?}",
×
2889
                draw_entity,
×
2890
                draw_batch.group_index,
×
2891
                draw_batch.batches_entity,
×
2892
            );
2893

2894
            // Get the EffectBatches this EffectDrawBatch is part of.
2895
            let Ok((batches_entity, batches)) = effect_batches.get(draw_batch.batches_entity)
×
2896
            else {
×
2897
                continue;
×
2898
            };
2899

2900
            trace!(
×
2901
                "-> EffectBaches: entity={:?} buffer_index={} spawner_base={} layout_flags={:?}",
×
2902
                batches_entity,
×
2903
                batches.buffer_index,
×
2904
                batches.spawner_base,
×
2905
                batches.layout_flags,
×
2906
            );
2907

2908
            if ParticleRenderAlphaMaskPipelineKey::from(batches.layout_flags) != alpha_mask {
×
NEW
2909
                trace!("Mismatching alpha mask pipeline key. Skipped.");
×
UNCOV
2910
                continue;
×
2911
            }
2912

2913
            // Check if batch contains any entity visible in the current view. Otherwise we
2914
            // can skip the entire batch. Note: This is O(n^2) but (unlike
2915
            // the Sprite renderer this is inspired from) we don't expect more than
2916
            // a handful of particle effect instances, so would rather not pay the memory
2917
            // cost of a FixedBitSet for the sake of an arguable speed-up.
2918
            // TODO - Profile to confirm.
2919
            #[cfg(feature = "trace")]
2920
            let _span_check_vis = bevy::utils::tracing::info_span!("check_visibility").entered();
×
2921
            let has_visible_entity = batches
×
2922
                .entities
×
2923
                .iter()
2924
                .any(|index| view_entities.contains(*index as usize));
×
2925
            if !has_visible_entity {
×
2926
                trace!("No visible entity for view, not emitting any draw call.");
×
2927
                continue;
×
2928
            }
2929
            #[cfg(feature = "trace")]
2930
            _span_check_vis.exit();
×
2931

2932
            // Create and cache the bind group layout for this texture layout
2933
            render_pipeline.cache_material(&batches.texture_layout);
×
2934

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

2938
            let local_space_simulation = batches
×
2939
                .layout_flags
×
2940
                .contains(LayoutFlags::LOCAL_SPACE_SIMULATION);
×
2941
            let alpha_mask = ParticleRenderAlphaMaskPipelineKey::from(batches.layout_flags);
×
2942
            let flipbook = batches.layout_flags.contains(LayoutFlags::FLIPBOOK);
×
2943
            let needs_uv = batches.layout_flags.contains(LayoutFlags::NEEDS_UV);
×
2944
            let needs_normal = batches.layout_flags.contains(LayoutFlags::NEEDS_NORMAL);
×
2945
            let ribbons = batches.layout_flags.contains(LayoutFlags::RIBBONS);
×
2946
            let image_count = batches.texture_layout.layout.len() as u8;
×
NEW
2947
            let render_mesh = render_meshes.get(&batches.mesh);
×
2948

2949
            // Specialize the render pipeline based on the effect batch
2950
            trace!(
×
2951
                "Specializing render pipeline: render_shaders={:?} image_count={} alpha_mask={:?} flipbook={:?} hdr={}",
×
2952
                batches.render_shaders,
×
2953
                image_count,
×
2954
                alpha_mask,
×
2955
                flipbook,
×
2956
                view.hdr
×
2957
            );
2958

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

2964
            let alpha_mode = batches.alpha_mode;
×
2965

NEW
2966
            let Some(mesh_layout) = render_mesh.map(|gpu_mesh| gpu_mesh.layout.clone()) else {
×
NEW
2967
                trace!("Missing mesh vertex buffer layout. Skipped.");
×
UNCOV
2968
                continue;
×
2969
            };
2970

2971
            #[cfg(feature = "trace")]
2972
            let _span_specialize = bevy::utils::tracing::info_span!("specialize").entered();
×
2973
            let render_pipeline_id = specialized_render_pipelines.specialize(
×
2974
                pipeline_cache,
×
2975
                render_pipeline,
×
2976
                ParticleRenderPipelineKey {
×
2977
                    shader: render_shader_source.clone(),
×
2978
                    mesh_layout: Some(mesh_layout),
×
2979
                    particle_layout: batches.particle_layout.clone(),
×
2980
                    texture_layout: batches.texture_layout.clone(),
×
2981
                    local_space_simulation,
×
2982
                    alpha_mask,
×
2983
                    alpha_mode,
×
2984
                    flipbook,
×
2985
                    needs_uv,
×
2986
                    needs_normal,
×
2987
                    ribbons,
×
2988
                    #[cfg(all(feature = "2d", feature = "3d"))]
×
2989
                    pipeline_mode,
×
NEW
2990
                    msaa_samples: msaa.samples(),
×
2991
                    hdr: view.hdr,
×
2992
                },
2993
            );
2994
            #[cfg(feature = "trace")]
2995
            _span_specialize.exit();
×
2996

2997
            trace!(
×
2998
                "+ Render pipeline specialized: id={:?} -> group_index={}",
×
2999
                render_pipeline_id,
×
3000
                draw_batch.group_index
×
3001
            );
3002
            trace!(
×
3003
                "+ Add Transparent for batch on draw_entity {:?}: buffer_index={} \
×
3004
                group_index={} spawner_base={} handle={:?}",
×
3005
                draw_entity,
×
3006
                batches.buffer_index,
×
3007
                draw_batch.group_index,
×
3008
                batches.spawner_base,
×
3009
                batches.handle
×
3010
            );
3011
            render_phase.add(
×
3012
                make_bin_key(render_pipeline_id, draw_batch, draw_batch.group_index, view),
×
NEW
3013
                (draw_entity, MainEntity::from(Entity::PLACEHOLDER)),
×
3014
                BinnedRenderPhaseType::NonMesh,
×
3015
            );
3016
        }
3017
    }
3018
}
3019

3020
#[allow(clippy::too_many_arguments)]
UNCOV
3021
pub(crate) fn queue_effects(
×
3022
    views: Query<(Entity, &RenderVisibleEntities, &ExtractedView, &Msaa)>,
3023
    effects_meta: Res<EffectsMeta>,
3024
    mut render_pipeline: ResMut<ParticlesRenderPipeline>,
3025
    mut specialized_render_pipelines: ResMut<SpecializedRenderPipelines<ParticlesRenderPipeline>>,
3026
    pipeline_cache: Res<PipelineCache>,
3027
    mut effect_bind_groups: ResMut<EffectBindGroups>,
3028
    effect_batches: Query<(Entity, &mut EffectBatches)>,
3029
    effect_draw_batches: Query<(Entity, &mut EffectDrawBatch)>,
3030
    events: Res<EffectAssetEvents>,
3031
    render_meshes: Res<RenderAssets<RenderMesh>>,
3032
    read_params: QueueEffectsReadOnlyParams,
3033
    mut view_entities: Local<FixedBitSet>,
3034
    #[cfg(feature = "2d")] mut transparent_2d_render_phases: ResMut<
3035
        ViewSortedRenderPhases<Transparent2d>,
3036
    >,
3037
    #[cfg(feature = "3d")] mut transparent_3d_render_phases: ResMut<
3038
        ViewSortedRenderPhases<Transparent3d>,
3039
    >,
3040
    #[cfg(feature = "3d")] mut alpha_mask_3d_render_phases: ResMut<
3041
        ViewBinnedRenderPhases<AlphaMask3d>,
3042
    >,
3043
) {
3044
    #[cfg(feature = "trace")]
UNCOV
3045
    let _span = bevy::utils::tracing::info_span!("hanabi:queue_effects").entered();
×
3046

3047
    trace!("queue_effects");
×
3048

3049
    // If an image has changed, the GpuImage has (probably) changed
UNCOV
3050
    for event in &events.images {
×
UNCOV
3051
        match event {
×
UNCOV
3052
            AssetEvent::Added { .. } => None,
×
3053
            AssetEvent::LoadedWithDependencies { .. } => None,
×
3054
            AssetEvent::Unused { .. } => None,
×
3055
            AssetEvent::Modified { id } => {
×
3056
                trace!("Destroy bind group of modified image asset {:?}", id);
×
3057
                effect_bind_groups.images.remove(id)
×
3058
            }
UNCOV
3059
            AssetEvent::Removed { id } => {
×
UNCOV
3060
                trace!("Destroy bind group of removed image asset {:?}", id);
×
UNCOV
3061
                effect_bind_groups.images.remove(id)
×
3062
            }
3063
        };
3064
    }
3065

UNCOV
3066
    if effects_meta.spawner_buffer.buffer().is_none() || effects_meta.spawner_buffer.is_empty() {
×
3067
        // No spawners are active
UNCOV
3068
        return;
×
3069
    }
3070

3071
    // Loop over all 2D cameras/views that need to render effects
3072
    #[cfg(feature = "2d")]
3073
    {
3074
        #[cfg(feature = "trace")]
3075
        let _span_draw = bevy::utils::tracing::info_span!("draw_2d").entered();
×
3076

3077
        let draw_effects_function_2d = read_params
3078
            .draw_functions_2d
3079
            .read()
3080
            .get_id::<DrawEffects>()
3081
            .unwrap();
3082

3083
        // Effects with full alpha blending
3084
        if !views.is_empty() {
3085
            trace!("Emit effect draw calls for alpha blended 2D views...");
×
3086
            emit_sorted_draw(
3087
                &views,
×
3088
                &mut transparent_2d_render_phases,
×
3089
                &mut view_entities,
×
3090
                &effect_batches,
×
3091
                &effect_draw_batches,
×
3092
                &mut render_pipeline,
×
3093
                specialized_render_pipelines.reborrow(),
×
3094
                &render_meshes,
×
3095
                &pipeline_cache,
×
3096
                |id, entity, draw_batch, _group, _view| Transparent2d {
×
3097
                    draw_function: draw_effects_function_2d,
×
3098
                    pipeline: id,
×
3099
                    entity,
×
3100
                    sort_key: draw_batch.z_sort_key_2d,
×
3101
                    batch_range: 0..1,
×
3102
                    extra_index: PhaseItemExtraIndex::NONE,
×
3103
                },
3104
                #[cfg(feature = "3d")]
3105
                PipelineMode::Camera2d,
3106
            );
3107
        }
3108
    }
3109

3110
    // Loop over all 3D cameras/views that need to render effects
3111
    #[cfg(feature = "3d")]
3112
    {
3113
        #[cfg(feature = "trace")]
3114
        let _span_draw = bevy::utils::tracing::info_span!("draw_3d").entered();
×
3115

3116
        // Effects with full alpha blending
3117
        if !views.is_empty() {
3118
            trace!("Emit effect draw calls for alpha blended 3D views...");
×
3119

3120
            let draw_effects_function_3d = read_params
×
3121
                .draw_functions_3d
×
3122
                .read()
3123
                .get_id::<DrawEffects>()
3124
                .unwrap();
3125

3126
            emit_sorted_draw(
3127
                &views,
×
3128
                &mut transparent_3d_render_phases,
×
3129
                &mut view_entities,
×
3130
                &effect_batches,
×
3131
                &effect_draw_batches,
×
3132
                &mut render_pipeline,
×
3133
                specialized_render_pipelines.reborrow(),
×
3134
                &render_meshes,
×
3135
                &pipeline_cache,
×
3136
                |id, entity, batch, _group, view| Transparent3d {
×
3137
                    draw_function: draw_effects_function_3d,
×
3138
                    pipeline: id,
×
3139
                    entity,
×
3140
                    distance: view
×
3141
                        .rangefinder3d()
×
3142
                        .distance_translation(&batch.translation_3d),
×
3143
                    batch_range: 0..1,
×
3144
                    extra_index: PhaseItemExtraIndex::NONE,
×
3145
                },
3146
                #[cfg(feature = "2d")]
3147
                PipelineMode::Camera3d,
3148
            );
3149
        }
3150

3151
        // Effects with alpha mask
3152
        if !views.is_empty() {
×
3153
            #[cfg(feature = "trace")]
3154
            let _span_draw = bevy::utils::tracing::info_span!("draw_alphamask").entered();
×
3155

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

3158
            let draw_effects_function_alpha_mask = read_params
×
3159
                .draw_functions_alpha_mask
×
3160
                .read()
3161
                .get_id::<DrawEffects>()
3162
                .unwrap();
3163

3164
            emit_binned_draw(
3165
                &views,
×
3166
                &mut alpha_mask_3d_render_phases,
×
3167
                &mut view_entities,
×
3168
                &effect_batches,
×
3169
                &effect_draw_batches,
×
3170
                &mut render_pipeline,
×
3171
                specialized_render_pipelines.reborrow(),
×
3172
                &pipeline_cache,
×
3173
                &render_meshes,
×
3174
                |id, _batch, _group, _view| OpaqueNoLightmap3dBinKey {
×
3175
                    pipeline: id,
×
3176
                    draw_function: draw_effects_function_alpha_mask,
×
3177
                    asset_id: AssetId::<Image>::default().untyped(),
×
3178
                    material_bind_group_id: None,
×
3179
                    // },
3180
                    // distance: view
3181
                    //     .rangefinder3d()
3182
                    //     .distance_translation(&batch.translation_3d),
3183
                    // batch_range: 0..1,
3184
                    // extra_index: PhaseItemExtraIndex::NONE,
3185
                },
3186
                #[cfg(feature = "2d")]
3187
                PipelineMode::Camera3d,
3188
                ParticleRenderAlphaMaskPipelineKey::AlphaMask,
3189
            );
3190
        }
3191

3192
        // Opaque particles
3193
        if !views.is_empty() {
×
3194
            #[cfg(feature = "trace")]
3195
            let _span_draw = bevy::utils::tracing::info_span!("draw_opaque").entered();
×
3196

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

3199
            let draw_effects_function_opaque = read_params
×
3200
                .draw_functions_opaque
×
3201
                .read()
3202
                .get_id::<DrawEffects>()
3203
                .unwrap();
3204

3205
            emit_binned_draw(
3206
                &views,
×
3207
                &mut alpha_mask_3d_render_phases,
×
3208
                &mut view_entities,
×
3209
                &effect_batches,
×
3210
                &effect_draw_batches,
×
3211
                &mut render_pipeline,
×
3212
                specialized_render_pipelines.reborrow(),
×
3213
                &pipeline_cache,
×
3214
                &render_meshes,
×
3215
                |id, _batch, _group, _view| OpaqueNoLightmap3dBinKey {
×
3216
                    pipeline: id,
×
3217
                    draw_function: draw_effects_function_opaque,
×
3218
                    asset_id: AssetId::<Image>::default().untyped(),
×
3219
                    material_bind_group_id: None,
×
3220
                    // },
3221
                    // distance: view
3222
                    //     .rangefinder3d()
3223
                    //     .distance_translation(&batch.translation_3d),
3224
                    // batch_range: 0..1,
3225
                    // extra_index: PhaseItemExtraIndex::NONE,
3226
                },
3227
                #[cfg(feature = "2d")]
3228
                PipelineMode::Camera3d,
3229
                ParticleRenderAlphaMaskPipelineKey::Opaque,
3230
            );
3231
        }
3232
    }
3233
}
3234

3235
/// Prepare GPU resources for effect rendering.
3236
///
3237
/// This system runs in the [`RenderSet::Prepare`] render set, after Bevy has
3238
/// updated the [`ViewUniforms`], which need to be referenced to get access to
3239
/// the current camera view.
UNCOV
3240
pub(crate) fn prepare_gpu_resources(
×
3241
    mut effects_meta: ResMut<EffectsMeta>,
3242
    render_device: Res<RenderDevice>,
3243
    view_uniforms: Res<ViewUniforms>,
3244
    render_pipeline: Res<ParticlesRenderPipeline>,
3245
) {
3246
    // Get the binding for the ViewUniform, the uniform data structure containing
3247
    // the Camera data for the current view. If not available, we cannot render
3248
    // anything.
UNCOV
3249
    let Some(view_binding) = view_uniforms.uniforms.binding() else {
×
3250
        return;
×
3251
    };
3252

3253
    // Create the bind group for the camera/view parameters
3254
    effects_meta.view_bind_group = Some(render_device.create_bind_group(
3255
        "hanabi:bind_group_camera_view",
3256
        &render_pipeline.view_layout,
3257
        &[
3258
            BindGroupEntry {
3259
                binding: 0,
3260
                resource: view_binding,
3261
            },
3262
            BindGroupEntry {
3263
                binding: 1,
3264
                resource: effects_meta.sim_params_uniforms.binding().unwrap(),
3265
            },
3266
        ],
3267
    ));
3268
}
3269

UNCOV
3270
pub(crate) fn prepare_bind_groups(
×
3271
    mut effects_meta: ResMut<EffectsMeta>,
3272
    mut effect_cache: ResMut<EffectCache>,
3273
    mut effect_bind_groups: ResMut<EffectBindGroups>,
3274
    effect_batches: Query<(Entity, &mut EffectBatches)>,
3275
    render_device: Res<RenderDevice>,
3276
    dispatch_indirect_pipeline: Res<DispatchIndirectPipeline>,
3277
    init_pipeline: Res<ParticlesInitPipeline>,
3278
    update_pipeline: Res<ParticlesUpdatePipeline>,
3279
    render_pipeline: ResMut<ParticlesRenderPipeline>,
3280
    gpu_images: Res<RenderAssets<GpuImage>>,
3281
) {
UNCOV
3282
    if effects_meta.spawner_buffer.is_empty() || effects_meta.spawner_buffer.buffer().is_none() {
×
UNCOV
3283
        return;
×
3284
    }
3285

3286
    {
3287
        #[cfg(feature = "trace")]
3288
        let _span = bevy::utils::tracing::info_span!("shared_bind_groups").entered();
×
3289

3290
        // Create the bind group for the global simulation parameters
3291
        if effects_meta.sim_params_bind_group.is_none() {
×
3292
            effects_meta.sim_params_bind_group = Some(render_device.create_bind_group(
×
3293
                "hanabi:bind_group_sim_params",
×
3294
                &update_pipeline.sim_params_layout, /* FIXME - Shared with vfx_update, is
×
3295
                                                     * that OK? */
×
3296
                &[BindGroupEntry {
×
3297
                    binding: 0,
×
3298
                    resource: effects_meta.sim_params_uniforms.binding().unwrap(),
×
3299
                }],
3300
            ));
3301
        }
3302

3303
        // Create the bind group for the spawner parameters
3304
        // FIXME - This is shared by init and update; should move
3305
        // "update_pipeline.spawner_buffer_layout" out of "update_pipeline"
3306
        trace!(
3307
            "Spawner buffer bind group: size={} aligned_size={}",
×
3308
            GpuSpawnerParams::min_size().get(),
×
3309
            effects_meta.spawner_buffer.aligned_size()
×
3310
        );
3311
        assert!(
×
3312
            effects_meta.spawner_buffer.aligned_size()
×
3313
                >= GpuSpawnerParams::min_size().get() as usize
×
3314
        );
3315
        // Note: we clear effects_meta.spawner_buffer each frame in prepare_effects(),
3316
        // so this bind group is always invalid at the minute and always needs
3317
        // re-creation.
3318
        effects_meta.spawner_bind_group = effects_meta.spawner_buffer.buffer().map(|buffer| {
×
3319
            render_device.create_bind_group(
×
3320
                "hanabi:bind_group_spawner_buffer",
×
3321
                &update_pipeline.spawner_buffer_layout, // FIXME - Shared with init,is that OK?
×
3322
                &[BindGroupEntry {
×
3323
                    binding: 0,
×
3324
                    resource: BindingResource::Buffer(BufferBinding {
×
3325
                        buffer,
×
3326
                        offset: 0,
×
3327
                        size: Some(
×
3328
                            NonZeroU64::new(effects_meta.spawner_buffer.aligned_size() as u64)
×
3329
                                .unwrap(),
×
3330
                        ),
3331
                    }),
3332
                }],
3333
            )
3334
        });
3335

3336
        // Create the bind group for the indirect dispatch of all effects
3337
        effects_meta.dr_indirect_bind_group = match (
×
3338
            effects_meta.render_effect_dispatch_buffer.buffer(),
×
3339
            effects_meta.render_group_dispatch_buffer.buffer(),
×
3340
            effects_meta.dispatch_indirect_buffer.buffer(),
×
3341
            effects_meta.particle_group_buffer.buffer(),
×
3342
            effects_meta.spawner_buffer.buffer(),
×
3343
        ) {
3344
            (
3345
                Some(render_effect_dispatch_buffer),
×
3346
                Some(render_group_dispatch_buffer),
×
3347
                Some(dispatch_indirect_buffer),
×
3348
                Some(particle_group_buffer),
×
3349
                Some(spawner_buffer),
×
3350
            ) => {
×
3351
                Some(render_device.create_bind_group(
×
3352
                    "hanabi:bind_group_vfx_indirect_dr_indirect",
×
3353
                    &dispatch_indirect_pipeline.dispatch_indirect_layout,
×
3354
                    &[
×
3355
                        BindGroupEntry {
×
3356
                            binding: 0,
×
3357
                            resource: BindingResource::Buffer(BufferBinding {
×
3358
                                buffer: render_effect_dispatch_buffer,
×
3359
                                offset: 0,
×
3360
                                size: None, //NonZeroU64::new(256), // Some(GpuRenderIndirect::min_size()),
×
3361
                            }),
3362
                        },
3363
                        BindGroupEntry {
×
3364
                            binding: 1,
×
3365
                            resource: BindingResource::Buffer(BufferBinding {
×
3366
                                buffer: render_group_dispatch_buffer,
×
3367
                                offset: 0,
×
3368
                                size: None, //NonZeroU64::new(256), // Some(GpuRenderIndirect::min_size()),
×
3369
                            }),
3370
                        },
3371
                        BindGroupEntry {
×
3372
                            binding: 2,
×
3373
                            resource: BindingResource::Buffer(BufferBinding {
×
3374
                                buffer: dispatch_indirect_buffer,
×
3375
                                offset: 0,
×
3376
                                size: None, //NonZeroU64::new(256), // Some(GpuDispatchIndirect::min_size()),
×
3377
                            }),
3378
                        },
3379
                        BindGroupEntry {
×
3380
                            binding: 3,
×
3381
                            resource: BindingResource::Buffer(BufferBinding {
×
3382
                                buffer: particle_group_buffer,
×
3383
                                offset: 0,
×
3384
                                size: None,
×
3385
                            }),
3386
                        },
3387
                        BindGroupEntry {
×
3388
                            binding: 4,
×
3389
                            resource: BindingResource::Buffer(BufferBinding {
×
3390
                                buffer: spawner_buffer,
×
3391
                                offset: 0,
×
3392
                                size: None,
×
3393
                            }),
3394
                        },
3395
                    ],
3396
                ))
3397
            }
3398
            _ => None,
×
3399
        };
3400

3401
        let (init_render_indirect_spawn_bind_group, init_render_indirect_clone_bind_group) = match (
×
3402
            effects_meta.render_effect_dispatch_buffer.buffer(),
3403
            effects_meta.render_group_dispatch_buffer.buffer(),
3404
        ) {
3405
            (Some(render_effect_dispatch_buffer), Some(render_group_dispatch_buffer)) => (
×
3406
                Some(render_device.create_bind_group(
×
3407
                    "hanabi:bind_group_init_render_dispatch_spawn",
×
3408
                    &init_pipeline.render_indirect_spawn_layout,
×
3409
                    &[
×
3410
                        BindGroupEntry {
×
3411
                            binding: 0,
×
3412
                            resource: BindingResource::Buffer(BufferBinding {
×
3413
                                buffer: render_effect_dispatch_buffer,
×
3414
                                offset: 0,
×
3415
                                size: Some(effects_meta.gpu_limits.render_effect_indirect_size()),
×
3416
                            }),
3417
                        },
3418
                        BindGroupEntry {
×
3419
                            binding: 1,
×
3420
                            resource: BindingResource::Buffer(BufferBinding {
×
3421
                                buffer: render_group_dispatch_buffer,
×
3422
                                offset: 0,
×
3423
                                size: Some(effects_meta.gpu_limits.render_group_indirect_size()),
×
3424
                            }),
3425
                        },
3426
                    ],
3427
                )),
3428
                Some(render_device.create_bind_group(
×
3429
                    "hanabi:bind_group_init_render_dispatch_clone",
×
3430
                    &init_pipeline.render_indirect_clone_layout,
×
3431
                    &[
×
3432
                        BindGroupEntry {
×
3433
                            binding: 0,
×
3434
                            resource: BindingResource::Buffer(BufferBinding {
×
3435
                                buffer: render_effect_dispatch_buffer,
×
3436
                                offset: 0,
×
3437
                                size: Some(effects_meta.gpu_limits.render_effect_indirect_size()),
×
3438
                            }),
3439
                        },
3440
                        BindGroupEntry {
×
3441
                            binding: 1,
×
3442
                            resource: BindingResource::Buffer(BufferBinding {
×
3443
                                buffer: render_group_dispatch_buffer,
×
3444
                                offset: 0,
×
3445
                                size: Some(effects_meta.gpu_limits.render_group_indirect_size()),
×
3446
                            }),
3447
                        },
3448
                        BindGroupEntry {
×
3449
                            binding: 2,
×
3450
                            resource: BindingResource::Buffer(BufferBinding {
×
3451
                                buffer: render_group_dispatch_buffer,
×
3452
                                offset: 0,
×
3453
                                size: Some(effects_meta.gpu_limits.render_group_indirect_size()),
×
3454
                            }),
3455
                        },
3456
                    ],
3457
                )),
3458
            ),
3459

3460
            (_, _) => (None, None),
×
3461
        };
3462

3463
        // Create the bind group for the indirect render buffer use in the init shader
3464
        effects_meta.init_render_indirect_spawn_bind_group = init_render_indirect_spawn_bind_group;
3465
        effects_meta.init_render_indirect_clone_bind_group = init_render_indirect_clone_bind_group;
3466
    }
3467

3468
    // Make a copy of the buffer ID before borrowing effects_meta mutably in the
3469
    // loop below
3470
    let Some(indirect_buffer) = effects_meta.dispatch_indirect_buffer.buffer().cloned() else {
×
3471
        return;
×
3472
    };
3473
    let Some(spawner_buffer) = effects_meta.spawner_buffer.buffer().cloned() else {
×
3474
        return;
×
3475
    };
3476

3477
    // Create the per-buffer bind groups
3478
    trace!("Create per-buffer bind groups...");
×
3479
    for (buffer_index, buffer) in effect_cache.buffers().iter().enumerate() {
×
3480
        #[cfg(feature = "trace")]
3481
        let _span_buffer = bevy::utils::tracing::info_span!("create_buffer_bind_groups").entered();
×
3482

3483
        let Some(buffer) = buffer else {
×
3484
            trace!(
×
3485
                "Effect buffer index #{} has no allocated EffectBuffer, skipped.",
×
3486
                buffer_index
3487
            );
3488
            continue;
×
3489
        };
3490

3491
        // Ensure all effects in this batch have a bind group for the entire buffer of
3492
        // the group, since the update phase runs on an entire group/buffer at
3493
        // once, with all the effect instances in it batched together.
3494
        trace!("effect particle buffer_index=#{}", buffer_index);
×
3495
        effect_bind_groups
×
3496
            .particle_buffers
×
3497
            .entry(buffer_index as u32)
×
3498
            .or_insert_with(|| {
×
3499
                trace!(
×
3500
                    "Create new particle bind groups for buffer_index={} | particle_layout {:?} | property_layout {:?}",
×
3501
                    buffer_index,
×
3502
                    buffer.particle_layout(),
×
3503
                    buffer.property_layout(),
×
3504
                );
3505

3506
                let dispatch_indirect_size = GpuDispatchIndirect::aligned_size(render_device
×
3507
                    .limits()
×
3508
                    .min_storage_buffer_offset_alignment);
×
3509
                let mut entries = vec![
×
3510
                    BindGroupEntry {
×
3511
                        binding: 0,
×
3512
                        resource: buffer.max_binding(),
×
3513
                    },
3514
                    BindGroupEntry {
×
3515
                        binding: 1,
×
3516
                        resource: buffer.indirect_max_binding(),
×
3517
                    },
3518
                    BindGroupEntry {
×
3519
                        binding: 2,
×
3520
                        resource: BindingResource::Buffer(BufferBinding {
×
3521
                            buffer: &indirect_buffer,
×
3522
                            offset: 0,
×
3523
                            size: Some(dispatch_indirect_size),
×
3524
                        }),
3525
                    },
3526
                ];
3527
                if buffer.layout_flags().contains(LayoutFlags::LOCAL_SPACE_SIMULATION) {
×
3528
                    entries.push(BindGroupEntry {
×
3529
                        binding: 3,
×
3530
                        resource: BindingResource::Buffer(BufferBinding {
×
3531
                            buffer: &spawner_buffer,
×
3532
                            offset: 0,
×
3533
                            size: Some(GpuSpawnerParams::min_size()),
×
3534
                        }),
3535
                    });
3536
                }
3537
                trace!("Creating render bind group with {} entries (layout flags: {:?})", entries.len(), buffer.layout_flags());
×
3538
                let render = render_device.create_bind_group(
×
3539
                    &format!("hanabi:bind_group:render_vfx{buffer_index}_particles")[..],
×
3540
                     buffer.particle_layout_bind_group_with_dispatch(),
×
3541
                     &entries,
×
3542
                );
3543

3544
                BufferBindGroups {
×
3545
                    render,
×
3546
                }
3547
            });
3548
    }
3549

3550
    // Create the per-effect bind groups.
3551
    for (entity, effect_batches) in effect_batches.iter() {
×
3552
        #[cfg(feature = "trace")]
3553
        let _span_buffer = bevy::utils::tracing::info_span!("create_batch_bind_groups").entered();
×
3554

3555
        let effect_cache_id = effect_batches.effect_cache_id;
3556

3557
        // Convert indirect buffer offsets from indices to bytes.
3558
        let first_effect_particle_group_buffer_offset = effects_meta
3559
            .gpu_limits
3560
            .particle_group_offset(effect_batches.first_particle_group_buffer_index)
3561
            as u64;
3562
        let effect_particle_groups_buffer_size = NonZeroU64::try_from(
3563
            u32::from(effects_meta.gpu_limits.particle_group_aligned_size) as u64
3564
                * effect_batches.group_batches.len() as u64,
3565
        )
3566
        .unwrap();
3567
        let group_binding = BufferBinding {
3568
            buffer: effects_meta.particle_group_buffer.buffer().unwrap(),
3569
            offset: first_effect_particle_group_buffer_offset,
3570
            size: Some(effect_particle_groups_buffer_size),
3571
        };
3572

3573
        let Some(Some(effect_buffer)) = effect_cache
×
3574
            .buffers_mut()
3575
            .get_mut(effect_batches.buffer_index as usize)
3576
        else {
3577
            error!("No particle buffer allocated for entity {:?}", entity);
×
3578
            continue;
×
3579
        };
3580

3581
        // Bind group for the init compute shader to simulate particles.
3582
        // TODO - move this creation in RenderSet::PrepareBindGroups
3583
        effect_buffer.create_sim_bind_group(
×
3584
            effect_batches.buffer_index,
×
3585
            &render_device,
×
3586
            group_binding,
×
3587
        );
3588

3589
        if effect_bind_groups
×
3590
            .update_render_indirect_bind_groups
×
3591
            .get(&effect_cache_id)
×
3592
            .is_none()
3593
        {
3594
            let DispatchBufferIndices {
3595
                render_effect_metadata_buffer_index: render_effect_dispatch_buffer_index,
×
NEW
3596
                render_group_dispatch_indices,
×
NEW
3597
                ..
×
NEW
3598
            } = &effect_batches.dispatch_buffer_indices;
×
3599
            let RenderGroupDispatchIndices::Allocated {
3600
                first_render_group_dispatch_buffer_index,
×
3601
                ..
NEW
3602
            } = render_group_dispatch_indices
×
3603
            else {
NEW
3604
                continue;
×
3605
            };
3606

3607
            let storage_alignment = effects_meta.gpu_limits.storage_buffer_align.get();
3608
            let render_effect_indirect_size =
3609
                GpuRenderEffectMetadata::aligned_size(storage_alignment);
3610
            let total_render_group_indirect_size = NonZeroU64::new(
3611
                GpuRenderGroupIndirect::aligned_size(storage_alignment).get()
3612
                    * effect_batches.group_batches.len() as u64,
3613
            )
3614
            .unwrap();
3615
            let particles_buffer_layout_update_render_indirect = render_device.create_bind_group(
3616
                "hanabi:bind_group_update_render_group_dispatch",
3617
                &update_pipeline.render_indirect_layout,
3618
                &[
3619
                    BindGroupEntry {
3620
                        binding: 0,
3621
                        resource: BindingResource::Buffer(BufferBinding {
3622
                            buffer: effects_meta.render_effect_dispatch_buffer.buffer().unwrap(),
3623
                            offset: effects_meta.gpu_limits.render_effect_indirect_offset(
3624
                                render_effect_dispatch_buffer_index.0,
3625
                            ),
3626
                            size: Some(render_effect_indirect_size),
3627
                        }),
3628
                    },
3629
                    BindGroupEntry {
3630
                        binding: 1,
3631
                        resource: BindingResource::Buffer(BufferBinding {
3632
                            buffer: effects_meta.render_group_dispatch_buffer.buffer().unwrap(),
3633
                            offset: effects_meta.gpu_limits.render_group_indirect_offset(
3634
                                first_render_group_dispatch_buffer_index.0,
3635
                            ),
3636
                            size: Some(total_render_group_indirect_size),
3637
                        }),
3638
                    },
3639
                ],
3640
            );
3641

3642
            trace!(
NEW
3643
                "Created new update render indirect bind group for effect #{:?}: \
×
NEW
3644
                render_effect={} \
×
NEW
3645
                render_group={} group_count={}",
×
NEW
3646
                effect_cache_id,
×
NEW
3647
                render_effect_dispatch_buffer_index.0,
×
NEW
3648
                first_render_group_dispatch_buffer_index.0,
×
NEW
3649
                effect_batches.group_batches.len()
×
3650
            );
3651

3652
            effect_bind_groups
×
3653
                .update_render_indirect_bind_groups
×
3654
                .insert(
3655
                    effect_cache_id,
×
3656
                    particles_buffer_layout_update_render_indirect,
×
3657
                );
3658
        }
3659

3660
        // Ensure the particle texture(s) are available as GPU resources and that a bind
3661
        // group for them exists
3662
        // FIXME fix this insert+get below
3663
        if !effect_batches.texture_layout.layout.is_empty() {
×
3664
            // This should always be available, as this is cached into the render pipeline
3665
            // just before we start specializing it.
3666
            let Some(material_bind_group_layout) =
×
3667
                render_pipeline.get_material(&effect_batches.texture_layout)
×
3668
            else {
3669
                error!(
×
3670
                    "Failed to find material bind group layout for buffer #{}",
×
3671
                    effect_batches.buffer_index
3672
                );
3673
                continue;
×
3674
            };
3675

3676
            // TODO = move
3677
            let material = Material {
3678
                layout: effect_batches.texture_layout.clone(),
3679
                textures: effect_batches.textures.iter().map(|h| h.id()).collect(),
×
3680
            };
3681
            assert_eq!(material.layout.layout.len(), material.textures.len());
3682

3683
            //let bind_group_entries = material.make_entries(&gpu_images).unwrap();
3684
            let Ok(bind_group_entries) = material.make_entries(&gpu_images) else {
×
3685
                trace!(
×
3686
                    "Temporarily ignoring material {:?} due to missing image(s)",
×
3687
                    material
3688
                );
3689
                continue;
×
3690
            };
3691

3692
            effect_bind_groups
3693
                .material_bind_groups
3694
                .entry(material.clone())
3695
                .or_insert_with(|| {
×
3696
                    debug!("Creating material bind group for material {:?}", material);
×
3697
                    render_device.create_bind_group(
×
3698
                        &format!(
×
3699
                            "hanabi:material_bind_group_{}",
×
3700
                            material.layout.layout.len()
×
3701
                        )[..],
×
3702
                        material_bind_group_layout,
×
3703
                        &bind_group_entries[..],
×
3704
                    )
3705
                });
3706
        }
3707
    }
3708
}
3709

3710
type DrawEffectsSystemState = SystemState<(
3711
    SRes<EffectsMeta>,
3712
    SRes<EffectBindGroups>,
3713
    SRes<PipelineCache>,
3714
    SRes<RenderAssets<RenderMesh>>,
3715
    SRes<MeshAllocator>,
3716
    SQuery<Read<ViewUniformOffset>>,
3717
    SQuery<Read<EffectBatches>>,
3718
    SQuery<Read<EffectDrawBatch>>,
3719
)>;
3720

3721
/// Draw function for rendering all active effects for the current frame.
3722
///
3723
/// Effects are rendered in the [`Transparent2d`] phase of the main 2D pass,
3724
/// and the [`Transparent3d`] phase of the main 3D pass.
3725
pub(crate) struct DrawEffects {
3726
    params: DrawEffectsSystemState,
3727
}
3728

3729
impl DrawEffects {
UNCOV
3730
    pub fn new(world: &mut World) -> Self {
×
3731
        Self {
UNCOV
3732
            params: SystemState::new(world),
×
3733
        }
3734
    }
3735
}
3736

3737
/// Draw all particles of a single effect in view, in 2D or 3D.
3738
///
3739
/// FIXME: use pipeline ID to look up which group index it is.
3740
fn draw<'w>(
×
3741
    world: &'w World,
3742
    pass: &mut TrackedRenderPass<'w>,
3743
    view: Entity,
3744
    entity: (Entity, MainEntity),
3745
    pipeline_id: CachedRenderPipelineId,
3746
    params: &mut DrawEffectsSystemState,
3747
) {
3748
    let (
×
3749
        effects_meta,
×
3750
        effect_bind_groups,
×
3751
        pipeline_cache,
×
3752
        meshes,
×
NEW
3753
        mesh_allocator,
×
3754
        views,
×
3755
        effects,
×
3756
        effect_draw_batches,
×
3757
    ) = params.get(world);
×
3758
    let view_uniform = views.get(view).unwrap();
×
3759
    let effects_meta = effects_meta.into_inner();
×
3760
    let effect_bind_groups = effect_bind_groups.into_inner();
×
3761
    let meshes = meshes.into_inner();
×
NEW
3762
    let mesh_allocator = mesh_allocator.into_inner();
×
NEW
3763
    let effect_draw_batch = effect_draw_batches.get(entity.0).unwrap();
×
UNCOV
3764
    let effect_batches = effects.get(effect_draw_batch.batches_entity).unwrap();
×
3765

3766
    let gpu_limits = &effects_meta.gpu_limits;
×
3767

3768
    let Some(pipeline) = pipeline_cache.into_inner().get_render_pipeline(pipeline_id) else {
×
3769
        return;
×
3770
    };
3771

3772
    trace!("render pass");
×
3773

3774
    pass.set_render_pipeline(pipeline);
×
3775

NEW
3776
    let Some(render_mesh): Option<&RenderMesh> = meshes.get(&effect_batches.mesh) else {
×
NEW
3777
        return;
×
3778
    };
NEW
3779
    let Some(vertex_buffer_slice) = mesh_allocator.mesh_vertex_slice(&effect_batches.mesh.id())
×
NEW
3780
    else {
×
NEW
3781
        return;
×
3782
    };
3783

NEW
3784
    let RenderGroupDispatchIndices::Allocated {
×
NEW
3785
        first_render_group_dispatch_buffer_index,
×
NEW
3786
        ..
×
NEW
3787
    } = &effect_batches
×
NEW
3788
        .dispatch_buffer_indices
×
NEW
3789
        .render_group_dispatch_indices
×
NEW
3790
    else {
×
UNCOV
3791
        return;
×
3792
    };
3793

3794
    // Vertex buffer containing the particle model to draw. Generally a quad.
3795
    // FIXME - need to upload "vertex_buffer_slice.range.start as i32" into
3796
    // "base_vertex" in the indirect struct...
NEW
3797
    assert_eq!(
×
NEW
3798
        effect_batches.mesh_buffer.id(),
×
NEW
3799
        vertex_buffer_slice.buffer.id()
×
3800
    );
NEW
3801
    assert_eq!(effect_batches.mesh_slice, vertex_buffer_slice.range);
×
NEW
3802
    pass.set_vertex_buffer(0, vertex_buffer_slice.buffer.slice(..));
×
3803

3804
    // View properties (camera matrix, etc.)
3805
    pass.set_bind_group(
×
3806
        0,
3807
        effects_meta.view_bind_group.as_ref().unwrap(),
×
3808
        &[view_uniform.offset],
×
3809
    );
3810

3811
    // Particles buffer
3812
    let dispatch_indirect_offset = gpu_limits.dispatch_indirect_offset(effect_batches.buffer_index);
×
3813
    trace!(
×
3814
        "set_bind_group(1): dispatch_indirect_offset={}",
×
3815
        dispatch_indirect_offset
×
3816
    );
3817
    let spawner_base = effect_batches.spawner_base;
×
3818
    let spawner_buffer_aligned = effects_meta.spawner_buffer.aligned_size();
×
3819
    assert!(spawner_buffer_aligned >= GpuSpawnerParams::min_size().get() as usize);
×
3820
    let spawner_offset = spawner_base * spawner_buffer_aligned as u32;
×
3821
    let dyn_uniform_indices: [u32; 2] = [dispatch_indirect_offset, spawner_offset];
×
3822
    let dyn_uniform_indices = if effect_batches
×
3823
        .layout_flags
×
3824
        .contains(LayoutFlags::LOCAL_SPACE_SIMULATION)
×
3825
    {
3826
        &dyn_uniform_indices
×
3827
    } else {
3828
        &dyn_uniform_indices[..1]
×
3829
    };
3830
    pass.set_bind_group(
×
3831
        1,
3832
        effect_bind_groups
×
3833
            .particle_render(effect_batches.buffer_index)
×
3834
            .unwrap(),
×
3835
        dyn_uniform_indices,
×
3836
    );
3837

3838
    // Particle texture
3839
    // TODO = move
3840
    let material = Material {
3841
        layout: effect_batches.texture_layout.clone(),
×
3842
        textures: effect_batches.textures.iter().map(|h| h.id()).collect(),
×
3843
    };
3844
    if !effect_batches.texture_layout.layout.is_empty() {
×
3845
        if let Some(bind_group) = effect_bind_groups.material_bind_groups.get(&material) {
×
3846
            pass.set_bind_group(2, bind_group, &[]);
×
3847
        } else {
3848
            // Texture(s) not ready; skip this drawing for now
3849
            trace!(
×
3850
                "Particle material bind group not available for batch buf={}. Skipping draw call.",
×
3851
                effect_batches.buffer_index,
×
3852
            );
3853
            return; // continue;
×
3854
        }
3855
    }
3856

3857
    let render_indirect_buffer = effects_meta.render_group_dispatch_buffer.buffer().unwrap();
×
3858
    let group_index = effect_draw_batch.group_index;
×
3859
    let effect_batch = &effect_batches.group_batches[group_index as usize];
×
3860

NEW
3861
    let render_group_dispatch_indirect_index =
×
NEW
3862
        first_render_group_dispatch_buffer_index.offset(group_index);
×
3863

3864
    trace!(
×
3865
        "Draw up to {} particles with {} vertices per particle for batch from buffer #{} \
×
3866
            (render_group_dispatch_indirect_index={:?}, group_index={}).",
×
3867
        effect_batch.slice.len(),
×
NEW
3868
        render_mesh.vertex_count,
×
3869
        effect_batches.buffer_index,
×
3870
        render_group_dispatch_indirect_index,
×
3871
        group_index,
×
3872
    );
3873

NEW
3874
    match render_mesh.buffer_info {
×
NEW
3875
        RenderMeshBufferInfo::Indexed {
×
3876
            count: _,
×
3877
            index_format,
×
3878
        } => {
×
NEW
3879
            let Some(index_buffer_slice) =
×
NEW
3880
                mesh_allocator.mesh_index_slice(&effect_batches.mesh.id())
×
NEW
3881
            else {
×
NEW
3882
                return;
×
3883
            };
3884

NEW
3885
            pass.set_index_buffer(index_buffer_slice.buffer.slice(..), 0, index_format);
×
3886

3887
            pass.draw_indexed_indirect(
×
3888
                render_indirect_buffer,
×
NEW
3889
                render_group_dispatch_indirect_index.0 as u64
×
3890
                    * u32::from(gpu_limits.render_group_indirect_aligned_size) as u64,
×
3891
            );
3892
        }
NEW
3893
        RenderMeshBufferInfo::NonIndexed => {
×
3894
            pass.draw_indirect(
×
3895
                render_indirect_buffer,
×
NEW
3896
                render_group_dispatch_indirect_index.0 as u64
×
3897
                    * u32::from(gpu_limits.render_group_indirect_aligned_size) as u64,
×
3898
            );
3899
        }
3900
    }
3901
}
3902

3903
#[cfg(feature = "2d")]
3904
impl Draw<Transparent2d> for DrawEffects {
3905
    fn draw<'w>(
×
3906
        &mut self,
3907
        world: &'w World,
3908
        pass: &mut TrackedRenderPass<'w>,
3909
        view: Entity,
3910
        item: &Transparent2d,
3911
    ) -> Result<(), DrawError> {
3912
        trace!("Draw<Transparent2d>: view={:?}", view);
×
3913
        draw(
3914
            world,
×
3915
            pass,
×
3916
            view,
×
3917
            item.entity,
×
3918
            item.pipeline,
×
3919
            &mut self.params,
×
3920
        );
NEW
3921
        Ok(())
×
3922
    }
3923
}
3924

3925
#[cfg(feature = "3d")]
3926
impl Draw<Transparent3d> for DrawEffects {
3927
    fn draw<'w>(
×
3928
        &mut self,
3929
        world: &'w World,
3930
        pass: &mut TrackedRenderPass<'w>,
3931
        view: Entity,
3932
        item: &Transparent3d,
3933
    ) -> Result<(), DrawError> {
3934
        trace!("Draw<Transparent3d>: view={:?}", view);
×
3935
        draw(
3936
            world,
×
3937
            pass,
×
3938
            view,
×
3939
            item.entity,
×
3940
            item.pipeline,
×
3941
            &mut self.params,
×
3942
        );
NEW
3943
        Ok(())
×
3944
    }
3945
}
3946

3947
#[cfg(feature = "3d")]
3948
impl Draw<AlphaMask3d> for DrawEffects {
3949
    fn draw<'w>(
×
3950
        &mut self,
3951
        world: &'w World,
3952
        pass: &mut TrackedRenderPass<'w>,
3953
        view: Entity,
3954
        item: &AlphaMask3d,
3955
    ) -> Result<(), DrawError> {
3956
        trace!("Draw<AlphaMask3d>: view={:?}", view);
×
3957
        draw(
3958
            world,
×
3959
            pass,
×
3960
            view,
×
3961
            item.representative_entity,
×
3962
            item.key.pipeline,
×
3963
            &mut self.params,
×
3964
        );
NEW
3965
        Ok(())
×
3966
    }
3967
}
3968

3969
#[cfg(feature = "3d")]
3970
impl Draw<Opaque3d> for DrawEffects {
3971
    fn draw<'w>(
×
3972
        &mut self,
3973
        world: &'w World,
3974
        pass: &mut TrackedRenderPass<'w>,
3975
        view: Entity,
3976
        item: &Opaque3d,
3977
    ) -> Result<(), DrawError> {
3978
        trace!("Draw<Opaque3d>: view={:?}", view);
×
3979
        draw(
3980
            world,
×
3981
            pass,
×
3982
            view,
×
3983
            item.representative_entity,
×
3984
            item.key.pipeline,
×
3985
            &mut self.params,
×
3986
        );
NEW
3987
        Ok(())
×
3988
    }
3989
}
3990

3991
fn create_init_particles_bind_group_layout(
×
3992
    render_device: &RenderDevice,
3993
    label: &str,
3994
    particle_layout_min_binding_size: NonZero<u64>,
3995
    property_layout_min_binding_size: Option<NonZero<u64>>,
3996
) -> BindGroupLayout {
3997
    let mut entries = Vec::with_capacity(3);
×
3998
    // (1,0) ParticleBuffer
3999
    entries.push(BindGroupLayoutEntry {
×
4000
        binding: 0,
×
4001
        visibility: ShaderStages::COMPUTE,
×
4002
        ty: BindingType::Buffer {
×
4003
            ty: BufferBindingType::Storage { read_only: false },
×
4004
            has_dynamic_offset: false,
×
4005
            min_binding_size: Some(particle_layout_min_binding_size),
×
4006
        },
4007
        count: None,
×
4008
    });
4009
    // (1,1) IndirectBuffer
4010
    entries.push(BindGroupLayoutEntry {
×
4011
        binding: 1,
×
4012
        visibility: ShaderStages::COMPUTE,
×
4013
        ty: BindingType::Buffer {
×
4014
            ty: BufferBindingType::Storage { read_only: false },
×
4015
            has_dynamic_offset: false,
×
4016
            min_binding_size: BufferSize::new(12),
×
4017
        },
4018
        count: None,
×
4019
    });
4020
    // (1,2) array<ParticleGroup>
4021
    let particle_group_size =
×
4022
        GpuParticleGroup::aligned_size(render_device.limits().min_storage_buffer_offset_alignment);
×
4023
    entries.push(BindGroupLayoutEntry {
×
4024
        binding: 2,
×
4025
        visibility: ShaderStages::COMPUTE,
×
4026
        ty: BindingType::Buffer {
×
4027
            ty: BufferBindingType::Storage { read_only: true },
×
4028
            has_dynamic_offset: false,
×
4029
            min_binding_size: Some(particle_group_size),
×
4030
        },
4031
        count: None,
×
4032
    });
4033
    if let Some(min_binding_size) = property_layout_min_binding_size {
×
4034
        // (1,3) Properties
4035
        entries.push(BindGroupLayoutEntry {
4036
            binding: 3,
4037
            visibility: ShaderStages::COMPUTE,
4038
            ty: BindingType::Buffer {
4039
                ty: BufferBindingType::Storage { read_only: true },
4040
                has_dynamic_offset: false, // TODO
4041
                min_binding_size: Some(min_binding_size),
4042
            },
4043
            count: None,
4044
        });
4045
    }
4046

4047
    trace!(
×
4048
        "Creating particle bind group layout '{}' for init pass with {} entries.",
×
4049
        label,
×
4050
        entries.len()
×
4051
    );
4052
    render_device.create_bind_group_layout(label, &entries)
×
4053
}
4054

UNCOV
4055
fn create_init_render_indirect_bind_group_layout(
×
4056
    render_device: &RenderDevice,
4057
    label: &str,
4058
    clone: bool,
4059
) -> BindGroupLayout {
UNCOV
4060
    let storage_alignment = render_device.limits().min_storage_buffer_offset_alignment;
×
UNCOV
4061
    let render_effect_indirect_size = GpuRenderEffectMetadata::aligned_size(storage_alignment);
×
UNCOV
4062
    let render_group_indirect_size = GpuRenderGroupIndirect::aligned_size(storage_alignment);
×
4063

UNCOV
4064
    let mut entries = vec![
×
4065
        // @binding(0) var<storage, read_write> render_effect_indirect :
4066
        // RenderEffectMetadata
UNCOV
4067
        BindGroupLayoutEntry {
×
UNCOV
4068
            binding: 0,
×
UNCOV
4069
            visibility: ShaderStages::COMPUTE,
×
UNCOV
4070
            ty: BindingType::Buffer {
×
UNCOV
4071
                ty: BufferBindingType::Storage { read_only: false },
×
UNCOV
4072
                has_dynamic_offset: true,
×
UNCOV
4073
                min_binding_size: Some(render_effect_indirect_size),
×
4074
            },
UNCOV
4075
            count: None,
×
4076
        },
4077
        // @binding(1) var<storage, read_write> dest_render_group_indirect : RenderGroupIndirect
UNCOV
4078
        BindGroupLayoutEntry {
×
UNCOV
4079
            binding: 1,
×
UNCOV
4080
            visibility: ShaderStages::COMPUTE,
×
UNCOV
4081
            ty: BindingType::Buffer {
×
UNCOV
4082
                ty: BufferBindingType::Storage { read_only: false },
×
UNCOV
4083
                has_dynamic_offset: true,
×
UNCOV
4084
                min_binding_size: Some(render_group_indirect_size),
×
4085
            },
UNCOV
4086
            count: None,
×
4087
        },
4088
    ];
4089

UNCOV
4090
    if clone {
×
4091
        // @binding(2) var<storage, read_write> src_render_group_indirect :
4092
        // RenderGroupIndirect
UNCOV
4093
        entries.push(BindGroupLayoutEntry {
×
UNCOV
4094
            binding: 2,
×
UNCOV
4095
            visibility: ShaderStages::COMPUTE,
×
UNCOV
4096
            ty: BindingType::Buffer {
×
UNCOV
4097
                ty: BufferBindingType::Storage { read_only: false },
×
UNCOV
4098
                has_dynamic_offset: true,
×
UNCOV
4099
                min_binding_size: Some(render_group_indirect_size),
×
4100
            },
UNCOV
4101
            count: None,
×
4102
        });
4103
    }
4104

UNCOV
4105
    render_device.create_bind_group_layout(label, &entries)
×
4106
}
4107

4108
fn create_update_bind_group_layout(
×
4109
    render_device: &RenderDevice,
4110
    label: &str,
4111
    particle_layout_min_binding_size: NonZero<u64>,
4112
    property_layout_min_binding_size: Option<NonZero<u64>>,
4113
) -> BindGroupLayout {
4114
    let particle_group_size =
×
4115
        GpuParticleGroup::aligned_size(render_device.limits().min_storage_buffer_offset_alignment);
×
4116
    let mut entries = vec![
×
4117
        // @binding(0) var<storage, read_write> particle_buffer : ParticleBuffer
4118
        BindGroupLayoutEntry {
×
4119
            binding: 0,
×
4120
            visibility: ShaderStages::COMPUTE,
×
4121
            ty: BindingType::Buffer {
×
4122
                ty: BufferBindingType::Storage { read_only: false },
×
4123
                has_dynamic_offset: false,
×
4124
                min_binding_size: Some(particle_layout_min_binding_size),
×
4125
            },
4126
            count: None,
×
4127
        },
4128
        // @binding(1) var<storage, read_write> indirect_buffer : IndirectBuffer
4129
        BindGroupLayoutEntry {
×
4130
            binding: 1,
×
4131
            visibility: ShaderStages::COMPUTE,
×
4132
            ty: BindingType::Buffer {
×
4133
                ty: BufferBindingType::Storage { read_only: false },
×
4134
                has_dynamic_offset: false,
×
4135
                min_binding_size: BufferSize::new(INDIRECT_INDEX_SIZE as _),
×
4136
            },
4137
            count: None,
×
4138
        },
4139
        // @binding(2) var<storage, read> particle_groups : array<ParticleGroup>
4140
        BindGroupLayoutEntry {
×
4141
            binding: 2,
×
4142
            visibility: ShaderStages::COMPUTE,
×
4143
            ty: BindingType::Buffer {
×
4144
                ty: BufferBindingType::Storage { read_only: true },
×
4145
                has_dynamic_offset: false,
×
4146
                min_binding_size: Some(particle_group_size),
×
4147
            },
4148
            count: None,
×
4149
        },
4150
    ];
4151
    if let Some(property_layout_min_binding_size) = property_layout_min_binding_size {
×
4152
        // @binding(3) var<storage, read> properties : Properties
4153
        entries.push(BindGroupLayoutEntry {
4154
            binding: 3,
4155
            visibility: ShaderStages::COMPUTE,
4156
            ty: BindingType::Buffer {
4157
                ty: BufferBindingType::Storage { read_only: true },
4158
                has_dynamic_offset: false, // TODO
4159
                min_binding_size: Some(property_layout_min_binding_size),
4160
            },
4161
            count: None,
4162
        });
4163
    }
4164

4165
    trace!(
×
4166
        "Creating particle bind group layout '{}' for update pass with {} entries.",
×
4167
        label,
×
4168
        entries.len()
×
4169
    );
4170
    render_device.create_bind_group_layout(label, &entries)
×
4171
}
4172

4173
/// Render node to run the simulation sub-graph once per frame.
4174
///
4175
/// This node doesn't simulate anything by itself, but instead schedules the
4176
/// simulation sub-graph, where other nodes like [`VfxSimulateNode`] do the
4177
/// actual simulation.
4178
///
4179
/// The simulation sub-graph is scheduled to run before the [`CameraDriverNode`]
4180
/// renders all the views, such that rendered views have access to the
4181
/// just-simulated particles to render them.
4182
///
4183
/// [`CameraDriverNode`]: bevy::render::camera::CameraDriverNode
4184
pub(crate) struct VfxSimulateDriverNode;
4185

4186
impl Node for VfxSimulateDriverNode {
UNCOV
4187
    fn run(
×
4188
        &self,
4189
        graph: &mut RenderGraphContext,
4190
        _render_context: &mut RenderContext,
4191
        _world: &World,
4192
    ) -> Result<(), NodeRunError> {
UNCOV
4193
        graph.run_sub_graph(
×
UNCOV
4194
            crate::plugin::simulate_graph::HanabiSimulateGraph,
×
UNCOV
4195
            vec![],
×
UNCOV
4196
            None,
×
4197
        )?;
UNCOV
4198
        Ok(())
×
4199
    }
4200
}
4201

4202
/// Render node to run the simulation of all effects once per frame.
4203
///
4204
/// Runs inside the simulation sub-graph, looping over all extracted effect
4205
/// batches to simulate them.
4206
pub(crate) struct VfxSimulateNode {
4207
    /// Query to retrieve the batches of effects to simulate and render.
4208
    effect_query: QueryState<(Entity, Read<EffectBatches>)>,
4209
}
4210

4211
impl VfxSimulateNode {
4212
    /// Create a new node for simulating the effects of the given world.
UNCOV
4213
    pub fn new(world: &mut World) -> Self {
×
4214
        Self {
UNCOV
4215
            effect_query: QueryState::new(world),
×
4216
        }
4217
    }
4218
}
4219

4220
impl Node for VfxSimulateNode {
UNCOV
4221
    fn input(&self) -> Vec<SlotInfo> {
×
UNCOV
4222
        vec![]
×
4223
    }
4224

UNCOV
4225
    fn update(&mut self, world: &mut World) {
×
UNCOV
4226
        trace!("VfxSimulateNode::update()");
×
UNCOV
4227
        self.effect_query.update_archetypes(world);
×
4228
    }
4229

UNCOV
4230
    fn run(
×
4231
        &self,
4232
        _graph: &mut RenderGraphContext,
4233
        render_context: &mut RenderContext,
4234
        world: &World,
4235
    ) -> Result<(), NodeRunError> {
UNCOV
4236
        trace!("VfxSimulateNode::run()");
×
4237

4238
        // Get the Entity containing the ViewEffectsEntity component used as container
4239
        // for the input data for this node.
4240
        // let view_entity = graph.get_input_entity(Self::IN_VIEW)?;
UNCOV
4241
        let pipeline_cache = world.resource::<PipelineCache>();
×
4242

UNCOV
4243
        let effects_meta = world.resource::<EffectsMeta>();
×
UNCOV
4244
        let effect_cache = world.resource::<EffectCache>();
×
UNCOV
4245
        let effect_bind_groups = world.resource::<EffectBindGroups>();
×
UNCOV
4246
        let dispatch_indirect_pipeline = world.resource::<DispatchIndirectPipeline>();
×
4247
        // let render_queue = world.resource::<RenderQueue>();
4248

4249
        // Make sure to schedule any buffer copy from changed effects before accessing
4250
        // them
4251
        {
UNCOV
4252
            let command_encoder = render_context.command_encoder();
×
UNCOV
4253
            effects_meta
×
UNCOV
4254
                .dispatch_indirect_buffer
×
UNCOV
4255
                .write_buffer(command_encoder);
×
UNCOV
4256
            effects_meta
×
UNCOV
4257
                .render_effect_dispatch_buffer
×
UNCOV
4258
                .write_buffer(command_encoder);
×
UNCOV
4259
            effects_meta
×
UNCOV
4260
                .render_group_dispatch_buffer
×
UNCOV
4261
                .write_buffer(command_encoder);
×
4262
        }
4263

4264
        // Compute init pass
4265
        // let mut total_group_count = 0;
4266
        {
4267
            {
UNCOV
4268
                trace!("init: loop over effect batches...");
×
4269

4270
                // Dispatch init compute jobs
UNCOV
4271
                for (entity, batches) in self.effect_query.iter_manual(world) {
×
4272
                    let RenderGroupDispatchIndices::Allocated {
NEW
4273
                        first_render_group_dispatch_buffer_index,
×
NEW
4274
                        trail_dispatch_buffer_indices,
×
NEW
4275
                    } = &batches
×
NEW
4276
                        .dispatch_buffer_indices
×
NEW
4277
                        .render_group_dispatch_indices
×
4278
                    else {
NEW
4279
                        continue;
×
4280
                    };
4281

4282
                    for &dest_group_index in batches.group_order.iter() {
×
4283
                        let initializer = &batches.initializers[dest_group_index as usize];
×
NEW
4284
                        let dest_render_group_dispatch_buffer_index =
×
NEW
4285
                            first_render_group_dispatch_buffer_index.offset(dest_group_index);
×
4286

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

4295
                        match initializer {
×
4296
                            EffectInitializer::Spawner(effect_spawner) => {
×
4297
                                let mut compute_pass = render_context
×
4298
                                    .command_encoder()
4299
                                    .begin_compute_pass(&ComputePassDescriptor {
×
4300
                                        label: Some("hanabi:init"),
×
4301
                                        timestamp_writes: None,
×
4302
                                    });
4303

4304
                                let render_effect_dispatch_buffer_index = batches
×
4305
                                    .dispatch_buffer_indices
×
4306
                                    .render_effect_metadata_buffer_index;
×
4307

4308
                                // FIXME - Currently we unconditionally count
4309
                                // all groups because the dispatch pass always
4310
                                // runs on all groups. We should consider if
4311
                                // it's worth skipping e.g. dormant or finished
4312
                                // effects at the cost of extra complexity.
4313
                                // total_group_count += batches.group_batches.len() as u32;
4314

4315
                                let Some(init_pipeline) = pipeline_cache.get_compute_pipeline(
×
4316
                                    batches.init_and_update_pipeline_ids[dest_group_index as usize]
4317
                                        .init,
4318
                                ) else {
4319
                                    if let CachedPipelineState::Err(err) = pipeline_cache
×
4320
                                        .get_compute_pipeline_state(
4321
                                            batches.init_and_update_pipeline_ids
×
4322
                                                [dest_group_index as usize]
×
4323
                                                .init,
×
4324
                                        )
4325
                                    {
4326
                                        error!(
4327
                                            "Failed to find init pipeline #{} for effect {:?}: \
×
4328
                                             {:?}",
×
4329
                                            batches.init_and_update_pipeline_ids
×
4330
                                                [dest_group_index as usize]
×
4331
                                                .init
×
4332
                                                .id(),
×
4333
                                            entity,
4334
                                            err
4335
                                        );
4336
                                    }
4337
                                    continue;
×
4338
                                };
4339

4340
                                // Do not dispatch any init work if there's nothing to spawn this
4341
                                // frame
4342
                                let spawn_count = effect_spawner.spawn_count;
4343
                                if spawn_count == 0 {
4344
                                    continue;
×
4345
                                }
4346

4347
                                const WORKGROUP_SIZE: u32 = 64;
4348
                                let workgroup_count = spawn_count.div_ceil(WORKGROUP_SIZE);
4349

4350
                                let effect_cache_id = batches.effect_cache_id;
4351

4352
                                // for (effect_entity, effect_slice) in
4353
                                // effects_meta.entity_map.iter()
4354
                                // Retrieve the ExtractedEffect from the entity
4355
                                // trace!("effect_entity={:?} effect_slice={:?}", effect_entity,
4356
                                // effect_slice); let effect =
4357
                                // self.effect_query.get_manual(world, *effect_entity).unwrap();
4358

4359
                                // Get the slice to init
4360
                                // let effect_slice = effects_meta.get(&effect_entity);
4361
                                // let effect_group =
4362
                                //     &effects_meta.effect_cache.buffers()[batch.buffer_index as
4363
                                // usize];
4364
                                let Some(particles_init_bind_group) =
×
4365
                                    effect_cache.init_bind_group(effect_cache_id)
4366
                                else {
4367
                                    error!(
×
4368
                                        "Failed to find init particle buffer bind group for \
×
4369
                                         entity {:?}",
×
4370
                                        entity
4371
                                    );
4372
                                    continue;
×
4373
                                };
4374

4375
                                let render_effect_indirect_offset =
4376
                                    effects_meta.gpu_limits.render_effect_indirect_offset(
4377
                                        render_effect_dispatch_buffer_index.0,
4378
                                    );
4379

4380
                                let render_group_indirect_offset =
4381
                                    effects_meta.gpu_limits.render_group_indirect_offset(
4382
                                        dest_render_group_dispatch_buffer_index.0,
4383
                                    );
4384

4385
                                trace!(
4386
                                    "record commands for init pipeline of effect {:?} \
×
4387
                                        (spawn {} = {} workgroups) spawner_base={} \
×
4388
                                        spawner_offset={} \
×
4389
                                        render_effect_indirect_offset={} \
×
4390
                                        first_render_group_indirect_offset={}...",
×
4391
                                    batches.handle,
4392
                                    spawn_count,
4393
                                    workgroup_count,
4394
                                    spawner_base,
4395
                                    spawner_offset,
4396
                                    render_effect_indirect_offset,
4397
                                    render_group_indirect_offset,
4398
                                );
4399

4400
                                // Setup compute pass
4401
                                compute_pass.set_pipeline(init_pipeline);
×
4402
                                compute_pass.set_bind_group(
×
4403
                                    0,
4404
                                    effects_meta.sim_params_bind_group.as_ref().unwrap(),
×
4405
                                    &[],
×
4406
                                );
4407
                                compute_pass.set_bind_group(1, particles_init_bind_group, &[]);
×
4408
                                compute_pass.set_bind_group(
×
4409
                                    2,
4410
                                    effects_meta.spawner_bind_group.as_ref().unwrap(),
×
4411
                                    &[spawner_offset],
×
4412
                                );
4413
                                compute_pass.set_bind_group(
×
4414
                                    3,
4415
                                    effects_meta
×
4416
                                        .init_render_indirect_spawn_bind_group
×
4417
                                        .as_ref()
×
4418
                                        .unwrap(),
×
4419
                                    &[
×
4420
                                        render_effect_indirect_offset as u32,
×
4421
                                        render_group_indirect_offset as u32,
×
4422
                                    ],
4423
                                );
4424
                                compute_pass.dispatch_workgroups(workgroup_count, 1, 1);
×
4425
                                trace!("init compute dispatched");
×
4426
                            }
4427

4428
                            EffectInitializer::Cloner(EffectCloner {
4429
                                cloner,
×
4430
                                clone_this_frame: spawn_this_frame,
×
4431
                                ..
×
4432
                            }) => {
×
4433
                                if !spawn_this_frame {
×
4434
                                    continue;
×
4435
                                }
4436

4437
                                let mut compute_pass = render_context
×
4438
                                    .command_encoder()
4439
                                    .begin_compute_pass(&ComputePassDescriptor {
×
4440
                                        label: Some("hanabi:clone"),
×
4441
                                        timestamp_writes: None,
×
4442
                                    });
4443

4444
                                let clone_pipeline_id = batches.init_and_update_pipeline_ids
×
4445
                                    [dest_group_index as usize]
×
4446
                                    .init;
×
4447

4448
                                let effect_cache_id = batches.effect_cache_id;
×
4449

4450
                                let Some(clone_pipeline) =
×
4451
                                    pipeline_cache.get_compute_pipeline(clone_pipeline_id)
4452
                                else {
4453
                                    if let CachedPipelineState::Err(err) =
×
4454
                                        pipeline_cache.get_compute_pipeline_state(clone_pipeline_id)
×
4455
                                    {
4456
                                        error!(
4457
                                            "Failed to find clone pipeline #{} for effect \
×
4458
                                                    {:?}: {:?}",
×
4459
                                            clone_pipeline_id.id(),
×
4460
                                            entity,
4461
                                            err
4462
                                        );
4463
                                    }
4464
                                    continue;
×
4465
                                };
4466

4467
                                let Some(particles_init_bind_group) =
×
4468
                                    effect_cache.init_bind_group(effect_cache_id)
4469
                                else {
4470
                                    error!(
×
4471
                                        "Failed to find clone particle buffer bind group \
×
4472
                                                 for entity {:?}, effect cache ID {:?}",
×
4473
                                        entity, effect_cache_id
4474
                                    );
4475
                                    continue;
×
4476
                                };
4477

4478
                                let render_effect_dispatch_buffer_index = batches
4479
                                    .dispatch_buffer_indices
4480
                                    .render_effect_metadata_buffer_index;
4481
                                let clone_dest_render_group_dispatch_buffer_index =
4482
                                    trail_dispatch_buffer_indices[&dest_group_index].dest;
4483
                                let clone_src_render_group_dispatch_buffer_index =
4484
                                    trail_dispatch_buffer_indices[&dest_group_index].src;
4485

4486
                                let render_effect_indirect_offset =
4487
                                    effects_meta.gpu_limits.render_effect_indirect_offset(
4488
                                        render_effect_dispatch_buffer_index.0,
4489
                                    );
4490

4491
                                let clone_dest_render_group_indirect_offset =
4492
                                    effects_meta.gpu_limits.render_group_indirect_offset(
4493
                                        clone_dest_render_group_dispatch_buffer_index.0,
4494
                                    );
4495
                                let clone_src_render_group_indirect_offset =
4496
                                    effects_meta.gpu_limits.render_group_indirect_offset(
4497
                                        clone_src_render_group_dispatch_buffer_index.0,
4498
                                    );
4499

4500
                                let first_update_group_dispatch_buffer_index = batches
4501
                                    .dispatch_buffer_indices
4502
                                    .first_update_group_dispatch_buffer_index;
4503

4504
                                let src_group_index = cloner.src_group_index;
4505
                                let update_src_group_dispatch_buffer_offset =
4506
                                    effects_meta.gpu_limits.dispatch_indirect_offset(
4507
                                        first_update_group_dispatch_buffer_index.0
4508
                                            + src_group_index,
4509
                                    );
4510

4511
                                compute_pass.set_pipeline(clone_pipeline);
4512
                                compute_pass.set_bind_group(
4513
                                    0,
4514
                                    effects_meta.sim_params_bind_group.as_ref().unwrap(),
4515
                                    &[],
4516
                                );
4517
                                compute_pass.set_bind_group(1, particles_init_bind_group, &[]);
4518
                                compute_pass.set_bind_group(
4519
                                    2,
4520
                                    effects_meta.spawner_bind_group.as_ref().unwrap(),
4521
                                    &[spawner_offset],
4522
                                );
4523
                                compute_pass.set_bind_group(
4524
                                    3,
4525
                                    effects_meta
4526
                                        .init_render_indirect_clone_bind_group
4527
                                        .as_ref()
4528
                                        .unwrap(),
4529
                                    &[
4530
                                        render_effect_indirect_offset as u32,
4531
                                        clone_dest_render_group_indirect_offset as u32,
4532
                                        clone_src_render_group_indirect_offset as u32,
4533
                                    ],
4534
                                );
4535

4536
                                if let Some(dispatch_indirect_buffer) =
×
4537
                                    effects_meta.dispatch_indirect_buffer.buffer()
4538
                                {
4539
                                    trace!(
4540
                                        "record commands for init clone pipeline of effect {:?} \
×
4541
                                            first_update_group_dispatch_buffer_index={} \
×
4542
                                            src_group_index={} \
×
4543
                                            update_src_group_dispatch_buffer_offset={}...",
×
4544
                                        batches.handle,
4545
                                        first_update_group_dispatch_buffer_index.0,
4546
                                        src_group_index,
4547
                                        update_src_group_dispatch_buffer_offset,
4548
                                    );
4549

4550
                                    compute_pass.dispatch_workgroups_indirect(
×
4551
                                        dispatch_indirect_buffer,
×
4552
                                        update_src_group_dispatch_buffer_offset as u64,
×
4553
                                    );
4554
                                }
4555
                                trace!("clone compute dispatched");
×
4556
                            }
4557
                        }
4558
                    }
4559
                }
4560
            }
4561
        }
4562

4563
        // Compute indirect dispatch pass
UNCOV
4564
        if effects_meta.spawner_buffer.buffer().is_some()
×
4565
            && !effects_meta.spawner_buffer.is_empty()
×
4566
            && effects_meta.dr_indirect_bind_group.is_some()
×
4567
            && effects_meta.sim_params_bind_group.is_some()
×
4568
        {
4569
            // Only start a compute pass if there's an effect; makes things clearer in
4570
            // debugger.
4571
            let mut compute_pass =
×
4572
                render_context
×
4573
                    .command_encoder()
4574
                    .begin_compute_pass(&ComputePassDescriptor {
×
4575
                        label: Some("hanabi:indirect_dispatch"),
×
4576
                        timestamp_writes: None,
×
4577
                    });
4578

4579
            // Dispatch indirect dispatch compute job
4580
            trace!("record commands for indirect dispatch pipeline...");
×
4581

4582
            // FIXME - The `vfx_indirect` shader assumes a contiguous array of ParticleGroup
4583
            // structures. So we need to pass the full array size, and we
4584
            // just update the unused groups for nothing. Otherwise we might
4585
            // update some unused group and miss some used ones, if there's any gap
4586
            // in the array.
4587
            const WORKGROUP_SIZE: u32 = 64;
4588
            let total_group_count = effects_meta.particle_group_buffer.len() as u32;
×
NEW
4589
            let workgroup_count = total_group_count.div_ceil(WORKGROUP_SIZE);
×
4590

4591
            // Setup compute pass
4592
            compute_pass.set_pipeline(&dispatch_indirect_pipeline.pipeline);
×
4593
            compute_pass.set_bind_group(
×
4594
                0,
4595
                // FIXME - got some unwrap() panic here, investigate... possibly race
4596
                // condition!
4597
                effects_meta.dr_indirect_bind_group.as_ref().unwrap(),
×
4598
                &[],
×
4599
            );
4600
            compute_pass.set_bind_group(
×
4601
                1,
4602
                effects_meta.sim_params_bind_group.as_ref().unwrap(),
×
4603
                &[],
×
4604
            );
4605
            compute_pass.dispatch_workgroups(workgroup_count, 1, 1);
×
4606
            trace!(
×
4607
                "indirect dispatch compute dispatched: num_batches={} workgroup_count={}",
×
4608
                total_group_count,
4609
                workgroup_count
4610
            );
4611
        }
4612

4613
        // Compute update pass
4614
        {
UNCOV
4615
            let mut compute_pass =
×
UNCOV
4616
                render_context
×
4617
                    .command_encoder()
UNCOV
4618
                    .begin_compute_pass(&ComputePassDescriptor {
×
UNCOV
4619
                        label: Some("hanabi:update"),
×
UNCOV
4620
                        timestamp_writes: None,
×
4621
                    });
4622

4623
            // Dispatch update compute jobs
4624
            for (entity, batches) in self.effect_query.iter_manual(world) {
×
4625
                let effect_cache_id = batches.effect_cache_id;
×
4626

4627
                let Some(particles_update_bind_group) =
×
4628
                    effect_cache.update_bind_group(effect_cache_id)
×
4629
                else {
4630
                    error!(
×
4631
                        "Failed to find update particle buffer bind group for entity {:?}, effect cache ID {:?}",
×
4632
                        entity, effect_cache_id
4633
                    );
4634
                    continue;
×
4635
                };
4636

4637
                let first_update_group_dispatch_buffer_index = batches
4638
                    .dispatch_buffer_indices
4639
                    .first_update_group_dispatch_buffer_index;
4640

NEW
4641
                let Some(update_render_indirect_bind_group) = effect_bind_groups
×
4642
                    .update_render_indirect_bind_groups
4643
                    .get(&effect_cache_id)
4644
                else {
4645
                    error!(
×
4646
                        "Failed to find update render indirect bind group for effect cache ID: {:?}, IDs present: {:?}",
×
4647
                        effect_cache_id,
×
4648
                        effect_bind_groups
×
4649
                            .update_render_indirect_bind_groups
×
4650
                            .keys()
×
4651
                            .collect::<Vec<_>>()
×
4652
                    );
4653
                    continue;
×
4654
                };
4655

4656
                for &group_index in batches.group_order.iter() {
×
4657
                    let init_and_update_pipeline_id =
×
4658
                        &batches.init_and_update_pipeline_ids[group_index as usize];
×
4659
                    let Some(update_pipeline) =
×
4660
                        pipeline_cache.get_compute_pipeline(init_and_update_pipeline_id.update)
×
4661
                    else {
4662
                        if let CachedPipelineState::Err(err) = pipeline_cache
×
4663
                            .get_compute_pipeline_state(init_and_update_pipeline_id.update)
×
4664
                        {
4665
                            error!(
4666
                                "Failed to find update pipeline #{} for effect {:?}, group {}: {:?}",
×
4667
                                init_and_update_pipeline_id.update.id(),
×
4668
                                entity,
4669
                                group_index,
4670
                                err
4671
                            );
4672
                        }
4673
                        continue;
×
4674
                    };
4675

4676
                    let update_group_dispatch_buffer_offset =
4677
                        effects_meta.gpu_limits.dispatch_indirect_offset(
4678
                            first_update_group_dispatch_buffer_index.0 + group_index,
4679
                        );
4680

4681
                    // Destination group spawners are packed one after one another.
4682
                    let spawner_base = batches.spawner_base + group_index;
4683
                    let spawner_buffer_aligned = effects_meta.spawner_buffer.aligned_size();
4684
                    assert!(spawner_buffer_aligned >= GpuSpawnerParams::min_size().get() as usize);
4685
                    let spawner_offset = spawner_base * spawner_buffer_aligned as u32;
×
4686

4687
                    // for (effect_entity, effect_slice) in effects_meta.entity_map.iter()
4688
                    // Retrieve the ExtractedEffect from the entity
4689
                    // trace!("effect_entity={:?} effect_slice={:?}", effect_entity,
4690
                    // effect_slice); let effect =
4691
                    // self.effect_query.get_manual(world, *effect_entity).unwrap();
4692

4693
                    // Get the slice to update
4694
                    // let effect_slice = effects_meta.get(&effect_entity);
4695
                    // let effect_group =
4696
                    //     &effects_meta.effect_cache.buffers()[batch.buffer_index as usize];
4697

4698
                    trace!(
×
4699
                        "record commands for update pipeline of effect {:?} \
×
4700
                        spawner_base={} update_group_dispatch_buffer_offset={}…",
×
4701
                        batches.handle,
4702
                        spawner_base,
4703
                        update_group_dispatch_buffer_offset,
4704
                    );
4705

4706
                    // Setup compute pass
4707
                    // compute_pass.set_pipeline(&effect_group.update_pipeline);
4708
                    compute_pass.set_pipeline(update_pipeline);
×
4709
                    compute_pass.set_bind_group(
×
4710
                        0,
4711
                        effects_meta.sim_params_bind_group.as_ref().unwrap(),
×
4712
                        &[],
×
4713
                    );
4714
                    compute_pass.set_bind_group(1, particles_update_bind_group, &[]);
×
4715
                    compute_pass.set_bind_group(
×
4716
                        2,
4717
                        effects_meta.spawner_bind_group.as_ref().unwrap(),
×
4718
                        &[spawner_offset],
×
4719
                    );
4720
                    compute_pass.set_bind_group(3, update_render_indirect_bind_group, &[]);
×
4721

4722
                    if let Some(buffer) = effects_meta.dispatch_indirect_buffer.buffer() {
×
4723
                        trace!(
4724
                            "dispatch_workgroups_indirect: buffer={:?} offset={}",
×
4725
                            buffer,
4726
                            update_group_dispatch_buffer_offset,
4727
                        );
4728
                        compute_pass.dispatch_workgroups_indirect(
×
4729
                            buffer,
×
4730
                            update_group_dispatch_buffer_offset as u64,
×
4731
                        );
4732
                        // TODO - offset
4733
                    }
4734

4735
                    trace!("update compute dispatched");
×
4736
                }
4737
            }
4738
        }
4739

UNCOV
4740
        Ok(())
×
4741
    }
4742
}
4743

4744
// FIXME - Remove this, handle it properly with a BufferTable::insert_many() or
4745
// so...
4746
fn allocate_sequential_buffers<T, I>(
×
4747
    buffer_table: &mut BufferTable<T>,
4748
    iterator: I,
4749
) -> BufferTableId
4750
where
4751
    T: Pod + ShaderSize,
4752
    I: Iterator<Item = T>,
4753
{
4754
    let mut first_buffer = None;
×
4755
    for (object_index, object) in iterator.enumerate() {
×
4756
        let buffer = buffer_table.insert(object);
×
4757
        match first_buffer {
×
4758
            None => first_buffer = Some(buffer),
×
4759
            Some(ref first_buffer) => {
×
4760
                if first_buffer.0 + object_index as u32 != buffer.0 {
×
4761
                    error!(
×
4762
                        "Allocator didn't allocate sequential indices (expected {:?}, got {:?}). \
×
4763
                        Expect trouble!",
×
4764
                        first_buffer.0 + object_index as u32,
×
4765
                        buffer.0
×
4766
                    );
4767
                }
4768
            }
4769
        }
4770
    }
4771

4772
    first_buffer.expect("No buffers allocated")
×
4773
}
4774

4775
impl From<LayoutFlags> for ParticleRenderAlphaMaskPipelineKey {
4776
    fn from(layout_flags: LayoutFlags) -> Self {
×
4777
        if layout_flags.contains(LayoutFlags::USE_ALPHA_MASK) {
×
4778
            ParticleRenderAlphaMaskPipelineKey::AlphaMask
×
4779
        } else if layout_flags.contains(LayoutFlags::OPAQUE) {
×
4780
            ParticleRenderAlphaMaskPipelineKey::Opaque
×
4781
        } else {
4782
            ParticleRenderAlphaMaskPipelineKey::Blend
×
4783
        }
4784
    }
4785
}
4786

4787
#[cfg(test)]
4788
mod tests {
4789
    use super::*;
4790

4791
    #[test]
4792
    fn layout_flags() {
4793
        let flags = LayoutFlags::default();
4794
        assert_eq!(flags, LayoutFlags::NONE);
4795
    }
4796

4797
    #[cfg(feature = "gpu_tests")]
4798
    #[test]
4799
    fn gpu_limits() {
4800
        use crate::test_utils::MockRenderer;
4801

4802
        let renderer = MockRenderer::new();
4803
        let device = renderer.device();
4804
        let limits = GpuLimits::from_device(&device);
4805

4806
        // assert!(limits.storage_buffer_align().get() >= 1);
4807
        assert!(
4808
            limits.render_effect_indirect_offset(256)
4809
                >= 256 * GpuRenderEffectMetadata::min_size().get()
4810
        );
4811
        assert!(
4812
            limits.render_group_indirect_offset(256)
4813
                >= 256 * GpuRenderGroupIndirect::min_size().get()
4814
        );
4815
        assert!(
4816
            limits.dispatch_indirect_offset(256) as u64
4817
                >= 256 * GpuDispatchIndirect::min_size().get()
4818
        );
4819
    }
4820
}
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