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

djeedai / bevy_hanabi / 10416573129

16 Aug 2024 07:49AM UTC coverage: 59.11% (-0.8%) from 59.863%
10416573129

push

github

web-flow
Add support for texture sampling as expression (#367)

Add a new `TextureSampleExpr` to allow sampling a texture from an
expression. Make the `ParticleTextureModifier` use this. Change the
texture image to be hosted on a new `EffectMaterial` component, and add
`Module::add_texture()` to define a new texture slot (binding).

This is a primitive first version with several restrictions:
- Only color textures (those returning `vec4<f32>`), no depth.
- Only sampling via `textureSample()` in WGSL, no LOD or bias, no
  unsampled gather, _etc._
- No validation whatsoever about the coherence of the slots, the images
  on the material, and the expressions. If anything is inconsistent,
  this might produce runtime errors (fail to create shader) or simply
  render the wrong thing (no texture).

Fixes #355

52 of 204 new or added lines in 8 files covered. (25.49%)

3 existing lines in 2 files now uncovered.

3494 of 5911 relevant lines covered (59.11%)

23.05 hits per line

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

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

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

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

64
mod aligned_buffer_vec;
65
mod batch;
66
mod buffer_table;
67
mod effect_cache;
68
mod shader_cache;
69

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

75
use self::batch::EffectBatches;
76

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

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

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

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

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

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

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

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

178
impl From<Mat4> for GpuCompressedTransform {
179
    fn from(value: Mat4) -> Self {
×
180
        let tr = value.transpose();
×
181
        #[cfg(test)]
182
        crate::test_utils::assert_approx_eq!(tr.w_axis, Vec4::W);
183
        Self {
184
            x_row: tr.x_axis,
×
185
            y_row: tr.y_axis,
×
186
            z_row: tr.z_axis,
×
187
        }
188
    }
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 GpuCompressedTransform {
205
    /// Returns the translation as represented by this transform.
206
    #[allow(dead_code)]
207
    pub fn translation(&self) -> Vec3 {
×
208
        Vec3 {
209
            x: self.x_row.w,
×
210
            y: self.y_row.w,
×
211
            z: self.z_row.w,
×
212
        }
213
    }
214
}
215

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

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

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

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

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

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

279
// FIXME - min_storage_buffer_offset_alignment
280
#[repr(C)]
281
#[derive(Debug, Clone, Copy, Pod, Zeroable, ShaderType)]
282
pub struct GpuDispatchIndirect {
283
    pub x: u32,
284
    pub y: u32,
285
    pub z: u32,
286
    pub pong: u32,
287
}
288

289
impl Default for GpuDispatchIndirect {
290
    fn default() -> Self {
×
291
        Self {
292
            x: 0,
293
            y: 1,
294
            z: 1,
295
            pong: 0,
296
        }
297
    }
298
}
299

300
#[repr(C)]
301
#[derive(Debug, Default, Clone, Copy, Pod, Zeroable, ShaderType)]
302
pub struct GpuRenderEffectMetadata {
303
    pub max_spawn: u32,
304
    pub ping: u32,
305
}
306

307
#[repr(C)]
308
#[derive(Debug, Default, Clone, Copy, Pod, Zeroable, ShaderType)]
309
pub struct GpuRenderGroupIndirect {
310
    pub vertex_count: u32,
311
    pub instance_count: u32,
312
    pub vertex_offset: i32,
313
    pub base_instance: u32,
314
    //
315
    pub alive_count: u32,
316
    pub max_update: u32,
317
    pub dead_count: u32,
318
}
319

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

345
/// Compute pipeline to run the `vfx_indirect` dispatch workgroup calculation
346
/// shader.
347
#[derive(Resource)]
348
pub(crate) struct DispatchIndirectPipeline {
349
    dispatch_indirect_layout: BindGroupLayout,
350
    pipeline: ComputePipeline,
351
}
352

353
impl FromWorld for DispatchIndirectPipeline {
354
    fn from_world(world: &mut World) -> Self {
1✔
355
        let render_device = world.get_resource::<RenderDevice>().unwrap();
1✔
356

357
        let storage_alignment = render_device.limits().min_storage_buffer_offset_alignment;
1✔
358
        let render_effect_indirect_size = GpuRenderEffectMetadata::aligned_size(storage_alignment);
1✔
359
        let render_group_indirect_size = GpuRenderGroupIndirect::aligned_size(storage_alignment);
1✔
360
        let dispatch_indirect_size = GpuDispatchIndirect::aligned_size(storage_alignment);
1✔
361
        let particle_group_size = GpuParticleGroup::aligned_size(storage_alignment);
1✔
362

363
        trace!(
1✔
364
            "GpuRenderEffectMetadata: min_size={} padded_size={} | GpuRenderGroupIndirect: min_size={} padded_size={} | \
×
365
            GpuDispatchIndirect: min_size={} padded_size={} | GpuParticleGroup: min_size={} padded_size={}",
×
366
            GpuRenderEffectMetadata::min_size(),
×
367
            render_effect_indirect_size,
×
368
            GpuRenderGroupIndirect::min_size(),
×
369
            render_group_indirect_size,
×
370
            GpuDispatchIndirect::min_size(),
×
371
            dispatch_indirect_size,
×
372
            GpuParticleGroup::min_size(),
×
373
            particle_group_size
374
        );
375
        let dispatch_indirect_layout = render_device.create_bind_group_layout(
1✔
376
            "hanabi:bind_group_layout:dispatch_indirect_dispatch_indirect",
377
            &[
1✔
378
                BindGroupLayoutEntry {
1✔
379
                    binding: 0,
1✔
380
                    visibility: ShaderStages::COMPUTE,
1✔
381
                    ty: BindingType::Buffer {
1✔
382
                        ty: BufferBindingType::Storage { read_only: false },
1✔
383
                        has_dynamic_offset: false,
1✔
384
                        min_binding_size: Some(render_effect_indirect_size),
1✔
385
                    },
386
                    count: None,
1✔
387
                },
388
                BindGroupLayoutEntry {
1✔
389
                    binding: 1,
1✔
390
                    visibility: ShaderStages::COMPUTE,
1✔
391
                    ty: BindingType::Buffer {
1✔
392
                        ty: BufferBindingType::Storage { read_only: false },
1✔
393
                        has_dynamic_offset: false,
1✔
394
                        min_binding_size: Some(render_group_indirect_size),
1✔
395
                    },
396
                    count: None,
1✔
397
                },
398
                BindGroupLayoutEntry {
1✔
399
                    binding: 2,
1✔
400
                    visibility: ShaderStages::COMPUTE,
1✔
401
                    ty: BindingType::Buffer {
1✔
402
                        ty: BufferBindingType::Storage { read_only: false },
1✔
403
                        has_dynamic_offset: false,
1✔
404
                        min_binding_size: Some(dispatch_indirect_size),
1✔
405
                    },
406
                    count: None,
1✔
407
                },
408
                BindGroupLayoutEntry {
1✔
409
                    binding: 3,
1✔
410
                    visibility: ShaderStages::COMPUTE,
1✔
411
                    ty: BindingType::Buffer {
1✔
412
                        ty: BufferBindingType::Storage { read_only: true },
1✔
413
                        has_dynamic_offset: false,
1✔
414
                        min_binding_size: Some(particle_group_size),
1✔
415
                    },
416
                    count: None,
1✔
417
                },
418
            ],
419
        );
420

421
        trace!("GpuSimParams: min_size={}", GpuSimParams::min_size());
1✔
422
        let sim_params_layout = render_device.create_bind_group_layout(
1✔
423
            "hanabi:bind_group_layout:dispatch_indirect_sim_params",
424
            &[BindGroupLayoutEntry {
1✔
425
                binding: 0,
1✔
426
                visibility: ShaderStages::COMPUTE,
1✔
427
                ty: BindingType::Buffer {
1✔
428
                    ty: BufferBindingType::Uniform,
1✔
429
                    has_dynamic_offset: false,
1✔
430
                    min_binding_size: Some(GpuSimParams::min_size()),
1✔
431
                },
432
                count: None,
1✔
433
            }],
434
        );
435

436
        let pipeline_layout = render_device.create_pipeline_layout(&PipelineLayoutDescriptor {
1✔
437
            label: Some("hanabi:pipeline_layout:dispatch_indirect"),
1✔
438
            bind_group_layouts: &[&dispatch_indirect_layout, &sim_params_layout],
1✔
439
            push_constant_ranges: &[],
1✔
440
        });
441

442
        let render_effect_indirect_stride_code =
1✔
443
            (render_effect_indirect_size.get() as u32).to_wgsl_string();
1✔
444
        let render_group_indirect_stride_code =
1✔
445
            (render_group_indirect_size.get() as u32).to_wgsl_string();
1✔
446
        let dispatch_indirect_stride_code = (dispatch_indirect_size.get() as u32).to_wgsl_string();
1✔
447
        let indirect_code = include_str!("vfx_indirect.wgsl")
1✔
448
            .replace(
449
                "{{RENDER_EFFECT_INDIRECT_STRIDE}}",
450
                &render_effect_indirect_stride_code,
1✔
451
            )
452
            .replace(
453
                "{{RENDER_GROUP_INDIRECT_STRIDE}}",
454
                &render_group_indirect_stride_code,
1✔
455
            )
456
            .replace(
457
                "{{DISPATCH_INDIRECT_STRIDE}}",
458
                &dispatch_indirect_stride_code,
1✔
459
            );
460

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

466
            // Import bevy_hanabi::vfx_common
467
            {
468
                let common_shader = HanabiPlugin::make_common_shader(
469
                    render_device.limits().min_storage_buffer_offset_alignment,
470
                );
471
                let mut desc: naga_oil::compose::ComposableModuleDescriptor<'_> =
472
                    (&common_shader).into();
473
                desc.shader_defs.insert(
474
                    "SPAWNER_PADDING".to_string(),
475
                    naga_oil::compose::ShaderDefValue::Bool(true),
476
                );
477
                let res = composer.add_composable_module(desc);
478
                assert!(res.is_ok());
479
            }
480

481
            let shader_defs = default();
1✔
482

483
            match composer.make_naga_module(NagaModuleDescriptor {
1✔
484
                source: &indirect_code,
1✔
485
                file_path: "vfx_indirect.wgsl",
1✔
486
                shader_defs,
1✔
487
                ..Default::default()
1✔
488
            }) {
489
                Ok(naga_module) => ShaderSource::Naga(Cow::Owned(naga_module)),
490
                Err(compose_error) => panic!(
×
491
                    "Failed to compose vfx_indirect.wgsl, naga_oil returned: {}",
492
                    compose_error.emit_to_string(&composer)
×
493
                ),
494
            }
495
        };
496

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

499
        let shader_module = render_device.create_shader_module(ShaderModuleDescriptor {
1✔
500
            label: Some("hanabi:vfx_indirect_shader"),
1✔
501
            source: indirect_naga_module,
1✔
502
        });
503

504
        let pipeline = render_device.create_compute_pipeline(&RawComputePipelineDescriptor {
1✔
505
            label: Some("hanabi:compute_pipeline:dispatch_indirect"),
1✔
506
            layout: Some(&pipeline_layout),
1✔
507
            module: &shader_module,
1✔
508
            entry_point: "main",
1✔
509
            compilation_options: default(),
1✔
510
        });
511

512
        Self {
513
            dispatch_indirect_layout,
514
            pipeline,
515
        }
516
    }
517
}
518

519
#[derive(Resource)]
520
pub(crate) struct ParticlesInitPipeline {
521
    /// Render device the pipeline is attached to.
522
    render_device: RenderDevice,
523
    sim_params_layout: BindGroupLayout,
524
    spawner_buffer_layout: BindGroupLayout,
525
    render_indirect_layout: BindGroupLayout,
526
}
527

528
impl FromWorld for ParticlesInitPipeline {
529
    fn from_world(world: &mut World) -> Self {
1✔
530
        let render_device = world.get_resource::<RenderDevice>().unwrap();
1✔
531

532
        let limits = render_device.limits();
1✔
533
        bevy::log::info!(
1✔
534
            "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={}\n- max_storage_buffers_per_shader_stage={}\n- max_bind_groups={}",
1✔
535
            limits.max_compute_invocations_per_workgroup, limits.max_compute_workgroup_size_x, limits.max_compute_workgroup_size_y, limits.max_compute_workgroup_size_z,
536
            limits.max_compute_workgroups_per_dimension, limits.min_storage_buffer_offset_alignment, limits.max_storage_buffers_per_shader_stage, limits.max_bind_groups
537
        );
538

539
        trace!("GpuSimParams: min_size={}", GpuSimParams::min_size());
1✔
540
        let sim_params_layout = render_device.create_bind_group_layout(
1✔
541
            "hanabi:bind_group_layout:update_sim_params",
542
            &[BindGroupLayoutEntry {
1✔
543
                binding: 0,
1✔
544
                visibility: ShaderStages::COMPUTE,
1✔
545
                ty: BindingType::Buffer {
1✔
546
                    ty: BufferBindingType::Uniform,
1✔
547
                    has_dynamic_offset: false,
1✔
548
                    min_binding_size: Some(GpuSimParams::min_size()),
1✔
549
                },
550
                count: None,
1✔
551
            }],
552
        );
553

554
        trace!(
1✔
555
            "GpuSpawnerParams: min_size={}",
×
556
            GpuSpawnerParams::min_size()
×
557
        );
558
        let spawner_buffer_layout = render_device.create_bind_group_layout(
1✔
559
            "hanabi:buffer_layout:init_spawner",
560
            &[BindGroupLayoutEntry {
1✔
561
                binding: 0,
1✔
562
                visibility: ShaderStages::COMPUTE,
1✔
563
                ty: BindingType::Buffer {
1✔
564
                    ty: BufferBindingType::Storage { read_only: false },
1✔
565
                    has_dynamic_offset: true,
1✔
566
                    min_binding_size: Some(GpuSpawnerParams::min_size()),
1✔
567
                },
568
                count: None,
1✔
569
            }],
570
        );
571

572
        let storage_alignment = render_device.limits().min_storage_buffer_offset_alignment;
1✔
573
        let render_effect_indirect_size = GpuRenderEffectMetadata::aligned_size(storage_alignment);
1✔
574
        let render_group_indirect_size = GpuRenderGroupIndirect::aligned_size(storage_alignment);
1✔
575
        trace!(
1✔
576
            "GpuRenderEffectMetadata: min_size={} padded_size={}, GpuRenderGroupIndirect: min_size={} padded_size={}",
×
577
            GpuRenderEffectMetadata::min_size(),
×
578
            render_effect_indirect_size.get(),
×
579
            GpuRenderGroupIndirect::min_size(),
×
580
            render_group_indirect_size.get(),
×
581
        );
582
        let render_indirect_layout = render_device.create_bind_group_layout(
1✔
583
            "hanabi:bind_group_layout:init_render_indirect",
584
            &[
1✔
585
                // @binding(0) var<storage, read_write> render_effect_indirect :
586
                // RenderEffectMetadata
587
                BindGroupLayoutEntry {
1✔
588
                    binding: 0,
1✔
589
                    visibility: ShaderStages::COMPUTE,
1✔
590
                    ty: BindingType::Buffer {
1✔
591
                        ty: BufferBindingType::Storage { read_only: false },
1✔
592
                        has_dynamic_offset: true,
1✔
593
                        min_binding_size: Some(render_effect_indirect_size),
1✔
594
                    },
595
                    count: None,
1✔
596
                },
597
                // @binding(1) var<storage, read_write> render_group_indirect : RenderGroupIndirect
598
                BindGroupLayoutEntry {
1✔
599
                    binding: 1,
1✔
600
                    visibility: ShaderStages::COMPUTE,
1✔
601
                    ty: BindingType::Buffer {
1✔
602
                        ty: BufferBindingType::Storage { read_only: false },
1✔
603
                        has_dynamic_offset: true,
1✔
604
                        min_binding_size: Some(render_group_indirect_size),
1✔
605
                    },
606
                    count: None,
1✔
607
                },
608
            ],
609
        );
610

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

620
#[derive(Clone, Hash, PartialEq, Eq)]
621
pub(crate) struct ParticleInitPipelineKey {
622
    /// Compute shader, with snippets applied, but not preprocessed yet.
623
    shader: Handle<Shader>,
624
    /// Minimum binding size in bytes for the particle layout buffer.
625
    particle_layout_min_binding_size: NonZeroU64,
626
    /// Minimum binding size in bytes for the property layout buffer, if the
627
    /// effect has any property. Otherwise this is `None`.
628
    property_layout_min_binding_size: Option<NonZeroU64>,
629
}
630

631
impl SpecializedComputePipeline for ParticlesInitPipeline {
632
    type Key = ParticleInitPipelineKey;
633

634
    fn specialize(&self, key: Self::Key) -> ComputePipelineDescriptor {
×
635
        trace!(
×
636
            "GpuParticle: attributes.min_binding_size={} properties.min_binding_size={}",
×
637
            key.particle_layout_min_binding_size.get(),
×
638
            key.property_layout_min_binding_size
×
639
                .map(|sz| sz.get())
×
640
                .unwrap_or(0),
×
641
        );
642

643
        let mut entries = Vec::with_capacity(3);
×
644
        // (1,0) ParticleBuffer
645
        entries.push(BindGroupLayoutEntry {
×
646
            binding: 0,
×
647
            visibility: ShaderStages::COMPUTE,
×
648
            ty: BindingType::Buffer {
×
649
                ty: BufferBindingType::Storage { read_only: false },
×
650
                has_dynamic_offset: false,
×
651
                min_binding_size: Some(key.particle_layout_min_binding_size),
×
652
            },
653
            count: None,
×
654
        });
655
        // (1,1) IndirectBuffer
656
        entries.push(BindGroupLayoutEntry {
×
657
            binding: 1,
×
658
            visibility: ShaderStages::COMPUTE,
×
659
            ty: BindingType::Buffer {
×
660
                ty: BufferBindingType::Storage { read_only: false },
×
661
                has_dynamic_offset: false,
×
662
                min_binding_size: BufferSize::new(12),
×
663
            },
664
            count: None,
×
665
        });
666
        // (1,2) array<ParticleGroup>
667
        let particle_group_size = GpuParticleGroup::aligned_size(
668
            self.render_device
×
669
                .limits()
×
670
                .min_storage_buffer_offset_alignment,
×
671
        );
672
        entries.push(BindGroupLayoutEntry {
×
673
            binding: 2,
×
674
            visibility: ShaderStages::COMPUTE,
×
675
            ty: BindingType::Buffer {
×
676
                ty: BufferBindingType::Storage { read_only: true },
×
677
                has_dynamic_offset: false,
×
678
                min_binding_size: Some(particle_group_size),
×
679
            },
680
            count: None,
×
681
        });
682
        if let Some(min_binding_size) = key.property_layout_min_binding_size {
×
683
            // (1,3) Properties
684
            entries.push(BindGroupLayoutEntry {
685
                binding: 3,
686
                visibility: ShaderStages::COMPUTE,
687
                ty: BindingType::Buffer {
688
                    ty: BufferBindingType::Storage { read_only: true },
689
                    has_dynamic_offset: false, // TODO
690
                    min_binding_size: Some(min_binding_size),
691
                },
692
                count: None,
693
            });
694
        }
695

696
        let label = "hanabi:init_particles_buffer_layout";
697
        trace!(
698
            "Creating particle bind group layout '{}' for init pass with {} entries.",
×
699
            label,
×
700
            entries.len()
×
701
        );
702
        let particles_buffer_layout = self.render_device.create_bind_group_layout(label, &entries);
×
703

704
        ComputePipelineDescriptor {
705
            label: Some("hanabi:pipeline_init_compute".into()),
×
706
            layout: vec![
×
707
                self.sim_params_layout.clone(),
708
                particles_buffer_layout,
709
                self.spawner_buffer_layout.clone(),
710
                self.render_indirect_layout.clone(),
711
            ],
712
            shader: key.shader,
×
713
            shader_defs: vec![],
×
714
            entry_point: "main".into(),
×
715
            push_constant_ranges: Vec::new(),
×
716
        }
717
    }
718
}
719

720
#[derive(Resource)]
721
pub(crate) struct ParticlesUpdatePipeline {
722
    render_device: RenderDevice,
723
    sim_params_layout: BindGroupLayout,
724
    spawner_buffer_layout: BindGroupLayout,
725
    render_indirect_layout: BindGroupLayout,
726
}
727

728
impl FromWorld for ParticlesUpdatePipeline {
729
    fn from_world(world: &mut World) -> Self {
1✔
730
        let render_device = world.get_resource::<RenderDevice>().unwrap();
1✔
731

732
        let limits = render_device.limits();
1✔
733
        bevy::log::info!(
1✔
734
            "GPU limits:\n- max_compute_invocations_per_workgroup={}\n- max_compute_workgroup_size_x={}\n- max_compute_workgroup_size_y={}\n- max_compute_workgroup_size_z={}\n- max_compute_workgroups_per_dimension={}\n- min_storage_buffer_offset_alignment={}",
1✔
735
            limits.max_compute_invocations_per_workgroup, limits.max_compute_workgroup_size_x, limits.max_compute_workgroup_size_y, limits.max_compute_workgroup_size_z,
736
            limits.max_compute_workgroups_per_dimension, limits.min_storage_buffer_offset_alignment
737
        );
738

739
        trace!("GpuSimParams: min_size={}", GpuSimParams::min_size());
1✔
740
        let sim_params_layout = render_device.create_bind_group_layout(
1✔
741
            "hanabi:update_sim_params_layout",
742
            &[BindGroupLayoutEntry {
1✔
743
                binding: 0,
1✔
744
                visibility: ShaderStages::COMPUTE,
1✔
745
                ty: BindingType::Buffer {
1✔
746
                    ty: BufferBindingType::Uniform,
1✔
747
                    has_dynamic_offset: false,
1✔
748
                    min_binding_size: Some(GpuSimParams::min_size()),
1✔
749
                },
750
                count: None,
1✔
751
            }],
752
        );
753

754
        trace!(
1✔
755
            "GpuSpawnerParams: min_size={}",
×
756
            GpuSpawnerParams::min_size()
×
757
        );
758
        let spawner_buffer_layout = render_device.create_bind_group_layout(
1✔
759
            "hanabi:update_spawner_buffer_layout",
760
            &[BindGroupLayoutEntry {
1✔
761
                binding: 0,
1✔
762
                visibility: ShaderStages::COMPUTE,
1✔
763
                ty: BindingType::Buffer {
1✔
764
                    ty: BufferBindingType::Storage { read_only: false },
1✔
765
                    has_dynamic_offset: true,
1✔
766
                    min_binding_size: Some(GpuSpawnerParams::min_size()),
1✔
767
                },
768
                count: None,
1✔
769
            }],
770
        );
771

772
        let storage_alignment = render_device.limits().min_storage_buffer_offset_alignment;
1✔
773
        let render_effect_indirect_size = GpuRenderEffectMetadata::aligned_size(storage_alignment);
1✔
774
        let render_group_indirect_size = GpuRenderGroupIndirect::aligned_size(storage_alignment);
1✔
775
        trace!("GpuRenderEffectMetadata: min_size={} padded_size={} | GpuRenderGroupIndirect: min_size={} padded_size={}",
1✔
776
            GpuRenderEffectMetadata::min_size(),
×
777
            render_effect_indirect_size.get(),
×
778
            GpuRenderGroupIndirect::min_size(),
×
779
            render_group_indirect_size.get());
×
780
        let render_indirect_layout = render_device.create_bind_group_layout(
1✔
781
            "hanabi:update_render_indirect_layout",
782
            &[
1✔
783
                BindGroupLayoutEntry {
1✔
784
                    binding: 0,
1✔
785
                    visibility: ShaderStages::COMPUTE,
1✔
786
                    ty: BindingType::Buffer {
1✔
787
                        ty: BufferBindingType::Storage { read_only: false },
1✔
788
                        has_dynamic_offset: false,
1✔
789
                        min_binding_size: Some(render_effect_indirect_size),
1✔
790
                    },
791
                    count: None,
1✔
792
                },
793
                BindGroupLayoutEntry {
1✔
794
                    binding: 1,
1✔
795
                    visibility: ShaderStages::COMPUTE,
1✔
796
                    ty: BindingType::Buffer {
1✔
797
                        ty: BufferBindingType::Storage { read_only: false },
1✔
798
                        has_dynamic_offset: false,
1✔
799
                        // Array; needs padded size
800
                        min_binding_size: Some(render_group_indirect_size),
1✔
801
                    },
802
                    count: None,
1✔
803
                },
804
            ],
805
        );
806

807
        Self {
808
            render_device: render_device.clone(),
1✔
809
            sim_params_layout,
810
            spawner_buffer_layout,
811
            render_indirect_layout,
812
        }
813
    }
814
}
815

816
#[derive(Default, Clone, Hash, PartialEq, Eq)]
817
pub(crate) struct ParticleUpdatePipelineKey {
818
    /// Compute shader, with snippets applied, but not preprocessed yet.
819
    shader: Handle<Shader>,
820
    /// Particle layout.
821
    particle_layout: ParticleLayout,
822
    /// Property layout.
823
    property_layout: PropertyLayout,
824
}
825

826
impl SpecializedComputePipeline for ParticlesUpdatePipeline {
827
    type Key = ParticleUpdatePipelineKey;
828

829
    fn specialize(&self, key: Self::Key) -> ComputePipelineDescriptor {
×
830
        trace!(
×
831
            "GpuParticle: attributes.min_binding_size={} properties.min_binding_size={}",
×
832
            key.particle_layout.min_binding_size().get(),
×
833
            if key.property_layout.is_empty() {
×
834
                0
×
835
            } else {
836
                key.property_layout.min_binding_size().get()
×
837
            },
838
        );
839

840
        let particle_group_size = GpuParticleGroup::aligned_size(
841
            self.render_device
×
842
                .limits()
×
843
                .min_storage_buffer_offset_alignment,
×
844
        );
845
        let mut entries = vec![
×
846
            // @binding(0) var<storage, read_write> particle_buffer : ParticleBuffer
847
            BindGroupLayoutEntry {
×
848
                binding: 0,
×
849
                visibility: ShaderStages::COMPUTE,
×
850
                ty: BindingType::Buffer {
×
851
                    ty: BufferBindingType::Storage { read_only: false },
×
852
                    has_dynamic_offset: false,
×
853
                    min_binding_size: Some(key.particle_layout.min_binding_size()),
×
854
                },
855
                count: None,
×
856
            },
857
            // @binding(1) var<storage, read_write> indirect_buffer : IndirectBuffer
858
            BindGroupLayoutEntry {
×
859
                binding: 1,
×
860
                visibility: ShaderStages::COMPUTE,
×
861
                ty: BindingType::Buffer {
×
862
                    ty: BufferBindingType::Storage { read_only: false },
×
863
                    has_dynamic_offset: false,
×
864
                    min_binding_size: BufferSize::new(INDIRECT_INDEX_SIZE as _),
×
865
                },
866
                count: None,
×
867
            },
868
            // @binding(2) var<storage, read> particle_groups : array<ParticleGroup>
869
            BindGroupLayoutEntry {
×
870
                binding: 2,
×
871
                visibility: ShaderStages::COMPUTE,
×
872
                ty: BindingType::Buffer {
×
873
                    ty: BufferBindingType::Storage { read_only: true },
×
874
                    has_dynamic_offset: false,
×
875
                    min_binding_size: Some(particle_group_size),
×
876
                },
877
                count: None,
×
878
            },
879
        ];
880
        if !key.property_layout.is_empty() {
×
881
            // @binding(3) var<storage, read> properties : Properties
882
            entries.push(BindGroupLayoutEntry {
×
883
                binding: 3,
×
884
                visibility: ShaderStages::COMPUTE,
×
885
                ty: BindingType::Buffer {
×
886
                    ty: BufferBindingType::Storage { read_only: true },
×
887
                    has_dynamic_offset: false, // TODO
×
888
                    min_binding_size: Some(key.property_layout.min_binding_size()),
×
889
                },
890
                count: None,
×
891
            });
892
        }
893

894
        let label = "hanabi:update_particles_buffer_layout";
895
        trace!(
896
            "Creating particle bind group layout '{}' for update pass with {} entries.",
×
897
            label,
×
898
            entries.len()
×
899
        );
900
        let update_particles_buffer_layout =
×
901
            self.render_device.create_bind_group_layout(label, &entries);
×
902

903
        ComputePipelineDescriptor {
904
            label: Some("hanabi:pipeline_update_compute".into()),
×
905
            layout: vec![
×
906
                self.sim_params_layout.clone(),
907
                update_particles_buffer_layout,
908
                self.spawner_buffer_layout.clone(),
909
                self.render_indirect_layout.clone(),
910
            ],
911
            shader: key.shader,
×
912
            shader_defs: vec!["REM_MAX_SPAWN_ATOMIC".into()],
×
913
            entry_point: "main".into(),
×
914
            push_constant_ranges: Vec::new(),
×
915
        }
916
    }
917
}
918

919
#[derive(Resource)]
920
pub(crate) struct ParticlesRenderPipeline {
921
    render_device: RenderDevice,
922
    view_layout: BindGroupLayout,
923
    material_layouts: HashMap<TextureLayout, BindGroupLayout>,
924
}
925

926
impl ParticlesRenderPipeline {
927
    /// Cache a material, creating its bind group layout based on the texture
928
    /// layout.
NEW
929
    pub fn cache_material(&mut self, layout: &TextureLayout) {
×
NEW
930
        if layout.layout.is_empty() {
×
NEW
931
            return;
×
932
        }
933

934
        // FIXME - no current stable API to insert an entry into a HashMap only if it
935
        // doesn't exist, and without having to build a key (as opposed to a reference).
936
        // So do 2 lookups instead, to avoid having to clone the layout if it's already
937
        // cached (which should be the common case).
NEW
938
        if self.material_layouts.contains_key(layout) {
×
NEW
939
            return;
×
940
        }
941

NEW
942
        let mut entries = Vec::with_capacity(layout.layout.len() * 2);
×
NEW
943
        let mut index = 0;
×
NEW
944
        for _slot in &layout.layout {
×
NEW
945
            entries.push(BindGroupLayoutEntry {
×
NEW
946
                binding: index,
×
NEW
947
                visibility: ShaderStages::FRAGMENT,
×
NEW
948
                ty: BindingType::Texture {
×
NEW
949
                    multisampled: false,
×
NEW
950
                    sample_type: TextureSampleType::Float { filterable: true },
×
NEW
951
                    view_dimension: TextureViewDimension::D2,
×
952
                },
NEW
953
                count: None,
×
954
            });
NEW
955
            entries.push(BindGroupLayoutEntry {
×
NEW
956
                binding: index + 1,
×
NEW
957
                visibility: ShaderStages::FRAGMENT,
×
NEW
958
                ty: BindingType::Sampler(SamplerBindingType::Filtering),
×
NEW
959
                count: None,
×
960
            });
NEW
961
            index += 2;
×
962
        }
NEW
963
        let material_bind_group_layout = self
×
NEW
964
            .render_device
×
NEW
965
            .create_bind_group_layout("hanabi:material_layout_render", &entries[..]);
×
966

NEW
967
        self.material_layouts
×
NEW
968
            .insert(layout.clone(), material_bind_group_layout);
×
969
    }
970

971
    /// Retrieve a bind group layout for a cached material.
NEW
972
    pub fn get_material(&self, layout: &TextureLayout) -> Option<&BindGroupLayout> {
×
973
        // Prevent a hash and lookup for the trivial case of an empty layout
NEW
974
        if layout.layout.is_empty() {
×
NEW
975
            return None;
×
976
        }
977

NEW
978
        self.material_layouts.get(layout)
×
979
    }
980
}
981

982
impl FromWorld for ParticlesRenderPipeline {
983
    fn from_world(world: &mut World) -> Self {
1✔
984
        let render_device = world.get_resource::<RenderDevice>().unwrap();
1✔
985

986
        let view_layout = render_device.create_bind_group_layout(
1✔
987
            "hanabi:view_layout_render",
988
            &[
1✔
989
                BindGroupLayoutEntry {
1✔
990
                    binding: 0,
1✔
991
                    visibility: ShaderStages::VERTEX_FRAGMENT,
1✔
992
                    ty: BindingType::Buffer {
1✔
993
                        ty: BufferBindingType::Uniform,
1✔
994
                        has_dynamic_offset: true,
1✔
995
                        min_binding_size: Some(ViewUniform::min_size()),
1✔
996
                    },
997
                    count: None,
1✔
998
                },
999
                BindGroupLayoutEntry {
1✔
1000
                    binding: 1,
1✔
1001
                    visibility: ShaderStages::VERTEX_FRAGMENT,
1✔
1002
                    ty: BindingType::Buffer {
1✔
1003
                        ty: BufferBindingType::Uniform,
1✔
1004
                        has_dynamic_offset: false,
1✔
1005
                        min_binding_size: Some(GpuSimParams::min_size()),
1✔
1006
                    },
1007
                    count: None,
1✔
1008
                },
1009
            ],
1010
        );
1011

1012
        Self {
1013
            render_device: render_device.clone(),
1✔
1014
            view_layout,
1015
            material_layouts: default(),
1✔
1016
        }
1017
    }
1018
}
1019

1020
#[cfg(all(feature = "2d", feature = "3d"))]
1021
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
1022
enum PipelineMode {
1023
    Camera2d,
1024
    Camera3d,
1025
}
1026

1027
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
1028
pub(crate) struct ParticleRenderPipelineKey {
1029
    /// Render shader, with snippets applied, but not preprocessed yet.
1030
    shader: Handle<Shader>,
1031
    /// Particle layout.
1032
    particle_layout: ParticleLayout,
1033
    /// Texture layout.
1034
    texture_layout: TextureLayout,
1035
    /// Key: LOCAL_SPACE_SIMULATION
1036
    /// The effect is simulated in local space, and during rendering all
1037
    /// particles are transformed by the effect's [`GlobalTransform`].
1038
    local_space_simulation: bool,
1039
    /// Key: USE_ALPHA_MASK
1040
    /// The effect is rendered with alpha masking.
1041
    use_alpha_mask: bool,
1042
    /// The effect needs Alpha blend.
1043
    alpha_mode: AlphaMode,
1044
    /// Key: FLIPBOOK
1045
    /// The effect is rendered with flipbook texture animation based on the
1046
    /// sprite index of each particle.
1047
    flipbook: bool,
1048
    /// Key: NEEDS_UV
1049
    /// The effect needs UVs.
1050
    needs_uv: bool,
1051
    /// For dual-mode configurations only, the actual mode of the current render
1052
    /// pipeline. Otherwise the mode is implicitly determined by the active
1053
    /// feature.
1054
    #[cfg(all(feature = "2d", feature = "3d"))]
1055
    pipeline_mode: PipelineMode,
1056
    /// MSAA sample count.
1057
    msaa_samples: u32,
1058
    /// Is the camera using an HDR render target?
1059
    hdr: bool,
1060
}
1061

1062
impl Default for ParticleRenderPipelineKey {
1063
    fn default() -> Self {
×
1064
        Self {
1065
            shader: Handle::default(),
×
1066
            particle_layout: ParticleLayout::empty(),
×
NEW
1067
            texture_layout: default(),
×
1068
            local_space_simulation: false,
1069
            use_alpha_mask: false,
1070
            alpha_mode: AlphaMode::Blend,
1071
            flipbook: false,
1072
            needs_uv: false,
1073
            #[cfg(all(feature = "2d", feature = "3d"))]
1074
            pipeline_mode: PipelineMode::Camera3d,
1075
            msaa_samples: Msaa::default().samples(),
×
1076
            hdr: false,
1077
        }
1078
    }
1079
}
1080

1081
impl SpecializedRenderPipeline for ParticlesRenderPipeline {
1082
    type Key = ParticleRenderPipelineKey;
1083

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

1087
        // Base mandatory part of vertex buffer layout
1088
        let vertex_buffer_layout = VertexBufferLayout {
1089
            array_stride: 20,
1090
            step_mode: VertexStepMode::Vertex,
1091
            attributes: vec![
×
1092
                //  @location(0) vertex_position: vec3<f32>
1093
                VertexAttribute {
1094
                    format: VertexFormat::Float32x3,
1095
                    offset: 0,
1096
                    shader_location: 0,
1097
                },
1098
                //  @location(1) vertex_uv: vec2<f32>
1099
                VertexAttribute {
1100
                    format: VertexFormat::Float32x2,
1101
                    offset: 12,
1102
                    shader_location: 1,
1103
                },
1104
                //  @location(1) vertex_color: u32
1105
                // VertexAttribute {
1106
                //     format: VertexFormat::Uint32,
1107
                //     offset: 12,
1108
                //     shader_location: 1,
1109
                // },
1110
                //  @location(2) vertex_velocity: vec3<f32>
1111
                // VertexAttribute {
1112
                //     format: VertexFormat::Float32x3,
1113
                //     offset: 12,
1114
                //     shader_location: 1,
1115
                // },
1116
                //  @location(3) vertex_uv: vec2<f32>
1117
                // VertexAttribute {
1118
                //     format: VertexFormat::Float32x2,
1119
                //     offset: 28,
1120
                //     shader_location: 3,
1121
                // },
1122
            ],
1123
        };
1124

1125
        let dispatch_indirect_size = GpuDispatchIndirect::aligned_size(
1126
            self.render_device
×
1127
                .limits()
×
1128
                .min_storage_buffer_offset_alignment,
×
1129
        );
1130
        let mut entries = vec![
×
1131
            BindGroupLayoutEntry {
×
1132
                binding: 0,
×
1133
                visibility: ShaderStages::VERTEX,
×
1134
                ty: BindingType::Buffer {
×
1135
                    ty: BufferBindingType::Storage { read_only: true },
×
1136
                    has_dynamic_offset: false,
×
1137
                    min_binding_size: Some(key.particle_layout.min_binding_size()),
×
1138
                },
1139
                count: None,
×
1140
            },
1141
            BindGroupLayoutEntry {
×
1142
                binding: 1,
×
1143
                visibility: ShaderStages::VERTEX,
×
1144
                ty: BindingType::Buffer {
×
1145
                    ty: BufferBindingType::Storage { read_only: true },
×
1146
                    has_dynamic_offset: false,
×
1147
                    min_binding_size: BufferSize::new(4u64),
×
1148
                },
1149
                count: None,
×
1150
            },
1151
            BindGroupLayoutEntry {
×
1152
                binding: 2,
×
1153
                visibility: ShaderStages::VERTEX,
×
1154
                ty: BindingType::Buffer {
×
1155
                    ty: BufferBindingType::Storage { read_only: true },
×
1156
                    has_dynamic_offset: true,
×
1157
                    min_binding_size: Some(dispatch_indirect_size),
×
1158
                },
1159
                count: None,
×
1160
            },
1161
        ];
1162
        if key.local_space_simulation {
×
1163
            entries.push(BindGroupLayoutEntry {
×
1164
                binding: 3,
×
1165
                visibility: ShaderStages::VERTEX,
×
1166
                ty: BindingType::Buffer {
×
1167
                    ty: BufferBindingType::Storage { read_only: true },
×
1168
                    has_dynamic_offset: true,
×
1169
                    min_binding_size: Some(GpuSpawnerParams::min_size()),
×
1170
                },
1171
                count: None,
×
1172
            });
1173
        }
1174

1175
        trace!(
1176
            "GpuParticle: layout.min_binding_size={}",
×
1177
            key.particle_layout.min_binding_size()
×
1178
        );
1179
        trace!(
×
1180
            "Creating render bind group layout with {} entries",
×
1181
            entries.len()
×
1182
        );
1183
        let particles_buffer_layout = self
×
1184
            .render_device
×
1185
            .create_bind_group_layout("hanabi:buffer_layout_render", &entries);
×
1186

1187
        let mut layout = vec![self.view_layout.clone(), particles_buffer_layout];
×
1188
        let mut shader_defs = vec!["SPAWNER_READONLY".into()];
×
1189

NEW
1190
        if let Some(material_bind_group_layout) = self.get_material(&key.texture_layout) {
×
1191
            layout.push(material_bind_group_layout.clone());
1192
            // //  @location(1) vertex_uv: vec2<f32>
1193
            // vertex_buffer_layout.attributes.push(VertexAttribute {
1194
            //     format: VertexFormat::Float32x2,
1195
            //     offset: 12,
1196
            //     shader_location: 1,
1197
            // });
1198
            // vertex_buffer_layout.array_stride += 8;
1199
        }
1200

1201
        // Key: LOCAL_SPACE_SIMULATION
1202
        if key.local_space_simulation {
×
1203
            shader_defs.push("LOCAL_SPACE_SIMULATION".into());
×
1204
            shader_defs.push("RENDER_NEEDS_SPAWNER".into());
×
1205
        }
1206

1207
        // Key: USE_ALPHA_MASK
1208
        if key.use_alpha_mask {
×
1209
            shader_defs.push("USE_ALPHA_MASK".into());
×
1210
        }
1211

1212
        // Key: FLIPBOOK
1213
        if key.flipbook {
×
1214
            shader_defs.push("FLIPBOOK".into());
×
1215
        }
1216

1217
        if key.needs_uv {
×
1218
            shader_defs.push("NEEDS_UV".into());
×
1219
        }
1220

1221
        #[cfg(all(feature = "2d", feature = "3d"))]
1222
        let depth_stencil = match key.pipeline_mode {
1223
            // Bevy's Transparent2d render phase doesn't support a depth-stencil buffer.
1224
            PipelineMode::Camera2d => None,
×
1225
            PipelineMode::Camera3d => Some(DepthStencilState {
×
1226
                format: TextureFormat::Depth32Float,
×
1227
                // Use depth buffer with alpha-masked particles, not with transparent ones
1228
                depth_write_enabled: key.use_alpha_mask,
×
1229
                // Bevy uses reverse-Z, so Greater really means closer
1230
                depth_compare: CompareFunction::Greater,
×
1231
                stencil: StencilState::default(),
×
1232
                bias: DepthBiasState::default(),
×
1233
            }),
1234
        };
1235

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

1239
        #[cfg(all(feature = "3d", not(feature = "2d")))]
1240
        let depth_stencil = Some(DepthStencilState {
1241
            format: TextureFormat::Depth32Float,
1242
            // Use depth buffer with alpha-masked particles, not with transparent ones
1243
            depth_write_enabled: key.use_alpha_mask,
1244
            // Bevy uses reverse-Z, so Greater really means closer
1245
            depth_compare: CompareFunction::Greater,
1246
            stencil: StencilState::default(),
1247
            bias: DepthBiasState::default(),
1248
        });
1249

1250
        let format = if key.hdr {
1251
            ViewTarget::TEXTURE_FORMAT_HDR
×
1252
        } else {
1253
            TextureFormat::bevy_default()
×
1254
        };
1255

1256
        let blend_state = match key.alpha_mode {
×
1257
            AlphaMode::Blend => BlendState::ALPHA_BLENDING,
×
1258
            AlphaMode::Premultiply => BlendState::PREMULTIPLIED_ALPHA_BLENDING,
×
1259
            AlphaMode::Add => BlendState {
1260
                color: BlendComponent {
×
1261
                    src_factor: BlendFactor::SrcAlpha,
1262
                    dst_factor: BlendFactor::One,
1263
                    operation: BlendOperation::Add,
1264
                },
1265
                alpha: BlendComponent {
×
1266
                    src_factor: BlendFactor::Zero,
1267
                    dst_factor: BlendFactor::One,
1268
                    operation: BlendOperation::Add,
1269
                },
1270
            },
1271
            AlphaMode::Multiply => BlendState {
1272
                color: BlendComponent {
×
1273
                    src_factor: BlendFactor::Dst,
1274
                    dst_factor: BlendFactor::OneMinusSrcAlpha,
1275
                    operation: BlendOperation::Add,
1276
                },
1277
                alpha: BlendComponent::OVER,
1278
            },
1279
            _ => BlendState::ALPHA_BLENDING,
×
1280
        };
1281

1282
        RenderPipelineDescriptor {
1283
            vertex: VertexState {
1284
                shader: key.shader.clone(),
1285
                entry_point: "vertex".into(),
1286
                shader_defs: shader_defs.clone(),
1287
                buffers: vec![vertex_buffer_layout],
1288
            },
1289
            fragment: Some(FragmentState {
1290
                shader: key.shader,
1291
                shader_defs,
1292
                entry_point: "fragment".into(),
1293
                targets: vec![Some(ColorTargetState {
1294
                    format,
1295
                    blend: Some(blend_state),
1296
                    write_mask: ColorWrites::ALL,
1297
                })],
1298
            }),
1299
            layout,
1300
            primitive: PrimitiveState {
1301
                front_face: FrontFace::Ccw,
1302
                cull_mode: None,
1303
                unclipped_depth: false,
1304
                polygon_mode: PolygonMode::Fill,
1305
                conservative: false,
1306
                topology: PrimitiveTopology::TriangleList,
1307
                strip_index_format: None,
1308
            },
1309
            depth_stencil,
1310
            multisample: MultisampleState {
1311
                count: key.msaa_samples,
1312
                mask: !0,
1313
                alpha_to_coverage_enabled: false,
1314
            },
1315
            label: Some("hanabi:pipeline_render".into()),
1316
            push_constant_ranges: Vec::new(),
1317
        }
1318
    }
1319
}
1320

1321
/// A single effect instance extracted from a [`ParticleEffect`] as a
1322
/// render world item.
1323
///
1324
/// [`ParticleEffect`]: crate::ParticleEffect
1325
#[derive(Debug, Component)]
1326
pub(crate) struct ExtractedEffect {
1327
    /// Handle to the effect asset this instance is based on.
1328
    /// The handle is weak to prevent refcount cycles and gracefully handle
1329
    /// assets unloaded or destroyed after a draw call has been submitted.
1330
    pub handle: Handle<EffectAsset>,
1331
    /// Particle layout for the effect.
1332
    #[allow(dead_code)]
1333
    pub particle_layout: ParticleLayout,
1334
    /// Property layout for the effect.
1335
    pub property_layout: PropertyLayout,
1336
    /// Values of properties written in a binary blob according to
1337
    /// [`property_layout`].
1338
    ///
1339
    /// This is `Some(blob)` if the data needs to be (re)uploaded to GPU, or
1340
    /// `None` if nothing needs to be done for this frame.
1341
    ///
1342
    /// [`property_layout`]: crate::render::ExtractedEffect::property_layout
1343
    pub property_data: Option<Vec<u8>>,
1344
    /// Number of particles to spawn this frame for the effect.
1345
    ///
1346
    /// Obtained from calling [`EffectSpawner::tick()`] on the source effect
1347
    /// instance.
1348
    ///
1349
    /// [`EffectSpawner::tick()`]: crate::EffectSpawner::tick
1350
    pub spawn_count: u32,
1351
    /// Global transform of the effect origin, extracted from the
1352
    /// [`GlobalTransform`].
1353
    pub transform: Mat4,
1354
    /// Inverse global transform of the effect origin, extracted from the
1355
    /// [`GlobalTransform`].
1356
    pub inverse_transform: Mat4,
1357
    /// Layout flags.
1358
    pub layout_flags: LayoutFlags,
1359
    /// Texture layout.
1360
    pub texture_layout: TextureLayout,
1361
    /// Textures.
1362
    pub textures: Vec<Handle<Image>>,
1363
    /// Alpha mode.
1364
    pub alpha_mode: AlphaMode,
1365
    /// Effect shader.
1366
    pub effect_shader: EffectShader,
1367
    /// For 2D rendering, the Z coordinate used as the sort key. Ignored for 3D
1368
    /// rendering.
1369
    #[cfg(feature = "2d")]
1370
    pub z_sort_key_2d: FloatOrd,
1371
}
1372

1373
/// Extracted data for newly-added [`ParticleEffect`] component requiring a new
1374
/// GPU allocation.
1375
///
1376
/// [`ParticleEffect`]: crate::ParticleEffect
1377
pub struct AddedEffect {
1378
    /// Entity with a newly-added [`ParticleEffect`] component.
1379
    ///
1380
    /// [`ParticleEffect`]: crate::ParticleEffect
1381
    pub entity: Entity,
1382
    /// Capacity of the effect (and therefore, the particle buffer), in number
1383
    /// of particles.
1384
    pub capacities: Vec<u32>,
1385
    /// Layout of particle attributes.
1386
    pub particle_layout: ParticleLayout,
1387
    /// Layout of properties for the effect, if properties are used at all, or
1388
    /// an empty layout.
1389
    pub property_layout: PropertyLayout,
1390
    pub layout_flags: LayoutFlags,
1391
    /// Handle of the effect asset.
1392
    pub handle: Handle<EffectAsset>,
1393
}
1394

1395
/// Collection of all extracted effects for this frame, inserted into the
1396
/// render world as a render resource.
1397
#[derive(Default, Resource)]
1398
pub(crate) struct ExtractedEffects {
1399
    /// Map of extracted effects from the entity the source [`ParticleEffect`]
1400
    /// is on.
1401
    ///
1402
    /// [`ParticleEffect`]: crate::ParticleEffect
1403
    pub effects: HashMap<Entity, ExtractedEffect>,
1404
    /// Entites which had their [`ParticleEffect`] component removed.
1405
    ///
1406
    /// [`ParticleEffect`]: crate::ParticleEffect
1407
    pub removed_effect_entities: Vec<Entity>,
1408
    /// Newly added effects without a GPU allocation yet.
1409
    pub added_effects: Vec<AddedEffect>,
1410
}
1411

1412
#[derive(Default, Resource)]
1413
pub(crate) struct EffectAssetEvents {
1414
    pub images: Vec<AssetEvent<Image>>,
1415
}
1416

1417
/// System extracting all the asset events for the [`Image`] assets to enable
1418
/// dynamic update of images bound to any effect.
1419
///
1420
/// This system runs in parallel of [`extract_effects`].
1421
pub(crate) fn extract_effect_events(
10✔
1422
    mut events: ResMut<EffectAssetEvents>,
1423
    mut image_events: Extract<EventReader<AssetEvent<Image>>>,
1424
) {
1425
    trace!("extract_effect_events");
10✔
1426

1427
    let EffectAssetEvents { ref mut images } = *events;
10✔
1428
    *images = image_events.read().copied().collect();
10✔
1429
}
1430

1431
/// System extracting data for rendering of all active [`ParticleEffect`]
1432
/// components.
1433
///
1434
/// Extract rendering data for all [`ParticleEffect`] components in the world
1435
/// which are visible ([`ComputedVisibility::is_visible`] is `true`), and wrap
1436
/// the data into a new [`ExtractedEffect`] instance added to the
1437
/// [`ExtractedEffects`] resource.
1438
///
1439
/// This system runs in parallel of [`extract_effect_events`].
1440
///
1441
/// [`ParticleEffect`]: crate::ParticleEffect
1442
pub(crate) fn extract_effects(
10✔
1443
    real_time: Extract<Res<Time<Real>>>,
1444
    virtual_time: Extract<Res<Time<Virtual>>>,
1445
    time: Extract<Res<Time<EffectSimulation>>>,
1446
    effects: Extract<Res<Assets<EffectAsset>>>,
1447
    _images: Extract<Res<Assets<Image>>>,
1448
    mut query: Extract<
1449
        ParamSet<(
1450
            // All existing ParticleEffect components
1451
            Query<(
1452
                Entity,
1453
                Option<&InheritedVisibility>,
1454
                Option<&ViewVisibility>,
1455
                &EffectSpawner,
1456
                &CompiledParticleEffect,
1457
                Option<Ref<EffectProperties>>,
1458
                &GlobalTransform,
1459
            )>,
1460
            // Newly added ParticleEffect components
1461
            Query<
1462
                (Entity, &CompiledParticleEffect),
1463
                (Added<CompiledParticleEffect>, With<GlobalTransform>),
1464
            >,
1465
        )>,
1466
    >,
1467
    mut removed_effects_event_reader: Extract<EventReader<RemovedEffectsEvent>>,
1468
    mut sim_params: ResMut<SimParams>,
1469
    mut extracted_effects: ResMut<ExtractedEffects>,
1470
) {
1471
    trace!("extract_effects");
10✔
1472

1473
    // Save simulation params into render world
1474
    sim_params.time = time.elapsed_seconds_f64();
10✔
1475
    sim_params.delta_time = time.delta_seconds();
10✔
1476
    sim_params.virtual_time = virtual_time.elapsed_seconds_f64();
10✔
1477
    sim_params.virtual_delta_time = virtual_time.delta_seconds();
10✔
1478
    sim_params.real_time = real_time.elapsed_seconds_f64();
10✔
1479
    sim_params.real_delta_time = real_time.delta_seconds();
10✔
1480

1481
    // Collect removed effects for later GPU data purge
1482
    extracted_effects.removed_effect_entities =
10✔
1483
        removed_effects_event_reader
10✔
1484
            .read()
10✔
1485
            .fold(vec![], |mut acc, ev| {
10✔
1486
                // FIXME - Need to clone because we can't consume the event, we only have
1487
                // read-only access to the main world
1488
                acc.append(&mut ev.entities.clone());
×
1489
                acc
×
1490
            });
1491
    trace!(
1492
        "Found {} removed entities.",
×
1493
        extracted_effects.removed_effect_entities.len()
×
1494
    );
1495

1496
    // Collect added effects for later GPU data allocation
1497
    extracted_effects.added_effects = query
10✔
1498
        .p1()
10✔
1499
        .iter()
10✔
1500
        .filter_map(|(entity, effect)| {
11✔
1501
            let handle = effect.asset.clone_weak();
1✔
1502
            let asset = effects.get(&effect.asset)?;
2✔
1503
            let particle_layout = asset.particle_layout();
1504
            assert!(
1505
                particle_layout.size() > 0,
1506
                "Invalid empty particle layout for effect '{}' on entity {:?}. Did you forget to add some modifier to the asset?",
×
1507
                asset.name,
1508
                entity
1509
            );
1510
            let property_layout = asset.property_layout();
×
1511

1512
            trace!("Found new effect: entity {:?} | capacities {:?} | particle_layout {:?} | property_layout {:?} | layout_flags {:?}", entity, asset.capacities(), particle_layout, property_layout, effect.layout_flags);
×
1513
            Some(AddedEffect {
×
1514
                entity,
×
1515
                capacities: asset.capacities().to_vec(),
×
1516
                particle_layout,
×
1517
                property_layout,
×
1518
                layout_flags: effect.layout_flags,
×
1519
                handle,
×
1520
            })
1521
        })
1522
        .collect();
1523

1524
    // Loop over all existing effects to update them
1525
    extracted_effects.effects.clear();
1526
    for (
1527
        entity,
×
1528
        maybe_inherited_visibility,
×
1529
        maybe_view_visibility,
×
1530
        spawner,
×
1531
        effect,
×
1532
        maybe_properties,
×
1533
        transform,
×
1534
    ) in query.p0().iter_mut()
1535
    {
1536
        // Check if shaders are configured
1537
        let Some(effect_shader) = effect.get_configured_shader() else {
×
1538
            continue;
×
1539
        };
1540

1541
        // Check if hidden, unless always simulated
1542
        if effect.simulation_condition == SimulationCondition::WhenVisible
1543
            && !maybe_inherited_visibility
×
1544
                .map(|cv| cv.get())
×
1545
                .unwrap_or(true)
×
1546
            && !maybe_view_visibility.map(|cv| cv.get()).unwrap_or(true)
×
1547
        {
1548
            continue;
×
1549
        }
1550

1551
        // Check if asset is available, otherwise silently ignore
1552
        let Some(asset) = effects.get(&effect.asset) else {
×
1553
            trace!(
×
1554
                "EffectAsset not ready; skipping ParticleEffect instance on entity {:?}.",
×
1555
                entity
1556
            );
1557
            continue;
×
1558
        };
1559

1560
        #[cfg(feature = "2d")]
1561
        let z_sort_key_2d = effect.z_layer_2d;
1562

1563
        let property_layout = asset.property_layout();
1564
        let texture_layout = asset.module().texture_layout();
1565

1566
        let property_data = if let Some(properties) = maybe_properties {
×
1567
            // Note: must check that property layout is not empty, because the
1568
            // EffectProperties component is marked as changed when added but contains an
1569
            // empty Vec if there's no property, which would later raise an error if we
1570
            // don't return None here.
1571
            if properties.is_changed() && !property_layout.is_empty() {
×
1572
                trace!("Detected property change, re-serializing...");
×
1573
                Some(properties.serialize(&property_layout))
×
1574
            } else {
1575
                None
×
1576
            }
1577
        } else {
1578
            None
×
1579
        };
1580

1581
        let layout_flags = effect.layout_flags;
1582
        let alpha_mode = effect.alpha_mode;
1583

1584
        trace!(
NEW
1585
            "Extracted instance of effect '{}' on entity {:?}: texture_layout_count={} texture_count={} layout_flags={:?}",
×
1586
            asset.name,
×
1587
            entity,
×
NEW
1588
            texture_layout.layout.len(),
×
NEW
1589
            effect.textures.len(),
×
1590
            layout_flags,
1591
        );
1592

1593
        extracted_effects.effects.insert(
×
1594
            entity,
×
1595
            ExtractedEffect {
×
1596
                handle: effect.asset.clone_weak(),
×
1597
                particle_layout: asset.particle_layout().clone(),
×
1598
                property_layout,
×
1599
                property_data,
×
1600
                spawn_count: spawner.spawn_count,
×
1601
                transform: transform.compute_matrix(),
×
1602
                // TODO - more efficient/correct way than inverse()?
1603
                inverse_transform: transform.compute_matrix().inverse(),
×
1604
                layout_flags,
×
NEW
1605
                texture_layout,
×
NEW
1606
                textures: effect.textures.clone(),
×
1607
                alpha_mode,
×
1608
                effect_shader,
×
1609
                #[cfg(feature = "2d")]
×
1610
                z_sort_key_2d,
×
1611
            },
1612
        );
1613
    }
1614
}
1615

1616
/// GPU representation of a single vertex of a particle mesh stored in a GPU
1617
/// buffer.
1618
#[repr(C)]
1619
#[derive(Copy, Clone, Pod, Zeroable, ShaderType)]
1620
struct GpuParticleVertex {
1621
    /// Vertex position.
1622
    pub position: [f32; 3],
1623
    /// UV coordinates of vertex.
1624
    pub uv: [f32; 2],
1625
}
1626

1627
/// Various GPU limits and aligned sizes computed once and cached.
1628
struct GpuLimits {
1629
    /// Value of [`WgpuLimits::min_storage_buffer_offset_alignment`].
1630
    ///
1631
    /// [`WgpuLimits::min_storage_buffer_offset_alignment`]: bevy::render::settings::WgpuLimits::min_storage_buffer_offset_alignment
1632
    storage_buffer_align: NonZeroU32,
1633
    /// Size of [`GpuDispatchIndirect`] aligned to the contraint of
1634
    /// [`WgpuLimits::min_storage_buffer_offset_alignment`].
1635
    ///
1636
    /// [`WgpuLimits::min_storage_buffer_offset_alignment`]: bevy::render::settings::WgpuLimits::min_storage_buffer_offset_alignment
1637
    dispatch_indirect_aligned_size: NonZeroU32,
1638
    render_effect_indirect_aligned_size: NonZeroU32,
1639
    /// Size of [`GpuRenderIndirect`] aligned to the contraint of
1640
    /// [`WgpuLimits::min_storage_buffer_offset_alignment`].
1641
    ///
1642
    /// [`WgpuLimits::min_storage_buffer_offset_alignment`]: bevy::render::settings::WgpuLimits::min_storage_buffer_offset_alignment
1643
    render_group_indirect_aligned_size: NonZeroU32,
1644
    particle_group_aligned_size: NonZeroU32,
1645
}
1646

1647
impl GpuLimits {
1648
    pub fn from_device(render_device: &RenderDevice) -> Self {
2✔
1649
        let storage_buffer_align = render_device.limits().min_storage_buffer_offset_alignment;
2✔
1650

1651
        let dispatch_indirect_aligned_size = NonZeroU32::new(next_multiple_of(
2✔
1652
            GpuDispatchIndirect::min_size().get() as usize,
2✔
1653
            storage_buffer_align as usize,
2✔
1654
        ) as u32)
2✔
1655
        .unwrap();
1656

1657
        let render_effect_indirect_aligned_size = NonZeroU32::new(next_multiple_of(
2✔
1658
            GpuRenderEffectMetadata::min_size().get() as usize,
2✔
1659
            storage_buffer_align as usize,
2✔
1660
        ) as u32)
2✔
1661
        .unwrap();
1662

1663
        let render_group_indirect_aligned_size = NonZeroU32::new(next_multiple_of(
2✔
1664
            GpuRenderGroupIndirect::min_size().get() as usize,
2✔
1665
            storage_buffer_align as usize,
2✔
1666
        ) as u32)
2✔
1667
        .unwrap();
1668

1669
        let particle_group_aligned_size = NonZeroU32::new(next_multiple_of(
2✔
1670
            GpuParticleGroup::min_size().get() as usize,
2✔
1671
            storage_buffer_align as usize,
2✔
1672
        ) as u32)
2✔
1673
        .unwrap();
1674

1675
        trace!(
2✔
1676
            "GpuLimits: storage_buffer_align={} gpu_dispatch_indirect_aligned_size={} \
×
1677
            gpu_render_effect_indirect_aligned_size={} gpu_render_group_indirect_aligned_size={}",
×
1678
            storage_buffer_align,
×
1679
            dispatch_indirect_aligned_size.get(),
×
1680
            render_effect_indirect_aligned_size.get(),
×
1681
            render_group_indirect_aligned_size.get()
×
1682
        );
1683

1684
        Self {
1685
            storage_buffer_align: NonZeroU32::new(storage_buffer_align).unwrap(),
2✔
1686
            dispatch_indirect_aligned_size,
1687
            render_effect_indirect_aligned_size,
1688
            render_group_indirect_aligned_size,
1689
            particle_group_aligned_size,
1690
        }
1691
    }
1692

1693
    /// Byte alignment for any storage buffer binding.
1694
    pub fn storage_buffer_align(&self) -> NonZeroU32 {
1✔
1695
        self.storage_buffer_align
1✔
1696
    }
1697

1698
    /// Byte alignment for [`GpuDispatchIndirect`].
1699
    pub fn dispatch_indirect_offset(&self, buffer_index: u32) -> u32 {
1✔
1700
        self.dispatch_indirect_aligned_size.get() * buffer_index
1✔
1701
    }
1702

1703
    /// Byte alignment for [`GpuRenderEffectMetadata`].
1704
    pub fn render_effect_indirect_offset(&self, buffer_index: u32) -> u64 {
1✔
1705
        self.render_effect_indirect_aligned_size.get() as u64 * buffer_index as u64
1✔
1706
    }
1707
    pub fn render_effect_indirect_size(&self) -> NonZeroU64 {
×
1708
        NonZeroU64::new(self.render_effect_indirect_aligned_size.get() as u64).unwrap()
×
1709
    }
1710

1711
    /// Byte alignment for [`GpuRenderGroupIndirect`].
1712
    pub fn render_group_indirect_offset(&self, buffer_index: u32) -> u64 {
1✔
1713
        self.render_group_indirect_aligned_size.get() as u64 * buffer_index as u64
1✔
1714
    }
1715
    pub fn render_group_indirect_size(&self) -> NonZeroU64 {
×
1716
        NonZeroU64::new(self.render_group_indirect_aligned_size.get() as u64).unwrap()
×
1717
    }
1718

1719
    /// Byte alignment for [`GpuParticleGroup`].
1720
    pub fn particle_group_offset(&self, buffer_index: u32) -> u32 {
×
1721
        self.particle_group_aligned_size.get() * buffer_index
×
1722
    }
1723
}
1724

1725
struct CacheEntry {
1726
    cache_id: EffectCacheId,
1727
}
1728

1729
/// Global resource containing the GPU data to draw all the particle effects in
1730
/// all views.
1731
///
1732
/// The resource is populated by [`prepare_effects()`] with all the effects to
1733
/// render for the current frame, for all views in the frame, and consumed by
1734
/// [`queue_effects()`] to actually enqueue the drawning commands to draw those
1735
/// effects.
1736
#[derive(Resource)]
1737
pub struct EffectsMeta {
1738
    /// Map from an entity with a [`ParticleEffect`] component attached to it,
1739
    /// to the associated effect slice allocated in the [`EffectCache`].
1740
    ///
1741
    /// [`ParticleEffect`]: crate::ParticleEffect
1742
    entity_map: HashMap<Entity, CacheEntry>,
1743
    /// Bind group for the camera view, containing the camera projection and
1744
    /// other uniform values related to the camera.
1745
    view_bind_group: Option<BindGroup>,
1746
    /// Bind group for the simulation parameters, like the current time and
1747
    /// frame delta time.
1748
    sim_params_bind_group: Option<BindGroup>,
1749
    /// Bind group for the spawning parameters (number of particles to spawn
1750
    /// this frame, ...).
1751
    spawner_bind_group: Option<BindGroup>,
1752
    /// Bind group #0 of the vfx_indirect shader, containing both the indirect
1753
    /// compute dispatch and render buffers.
1754
    dr_indirect_bind_group: Option<BindGroup>,
1755
    /// Bind group #3 of the vfx_init shader, containing the indirect render
1756
    /// buffer.
1757
    init_render_indirect_bind_group: Option<BindGroup>,
1758

1759
    sim_params_uniforms: UniformBuffer<GpuSimParams>,
1760
    spawner_buffer: AlignedBufferVec<GpuSpawnerParams>,
1761
    dispatch_indirect_buffer: BufferTable<GpuDispatchIndirect>,
1762
    /// Stores the GPU `RenderEffectMetadata` structures, which describe mutable
1763
    /// data relating to the entire effect.
1764
    render_effect_dispatch_buffer: BufferTable<GpuRenderEffectMetadata>,
1765
    /// Stores the GPU `RenderGroupIndirect` structures, which describe mutable
1766
    /// data specific to a particle group.
1767
    ///
1768
    /// These structures also store the data needed for indirect dispatch of
1769
    /// drawcalls.
1770
    render_group_dispatch_buffer: BufferTable<GpuRenderGroupIndirect>,
1771
    /// Stores the GPU `ParticleGroup` structures, which are metadata describing
1772
    /// each particle group that's populated by the CPU and read (only read) by
1773
    /// the GPU.
1774
    particle_group_buffer: AlignedBufferVec<GpuParticleGroup>,
1775
    /// Unscaled vertices of the mesh of a single particle, generally a quad.
1776
    /// The mesh is later scaled during rendering by the "particle size".
1777
    // FIXME - This is a per-effect thing, unless we merge all meshes into a single buffer (makes
1778
    // sense) but in that case we need a vertex slice too to know which mesh to draw per effect.
1779
    vertices: BufferVec<GpuParticleVertex>,
1780
    /// The pipeline for the indirect dispatch shader, which populates the
1781
    /// indirect compute dispatch buffers.
1782
    indirect_dispatch_pipeline: Option<ComputePipeline>,
1783
    /// Various GPU limits and aligned sizes lazily allocated and cached for
1784
    /// convenience.
1785
    gpu_limits: GpuLimits,
1786
}
1787

1788
impl EffectsMeta {
1789
    pub fn new(device: RenderDevice) -> Self {
1✔
1790
        let mut vertices = BufferVec::new(BufferUsages::VERTEX);
1✔
1791
        for v in QUAD_VERTEX_POSITIONS {
19✔
1792
            let uv = v.truncate() + 0.5;
6✔
1793
            let v = *v * Vec3::new(1.0, 1.0, 1.0);
6✔
1794
            vertices.push(GpuParticleVertex {
6✔
1795
                position: v.into(),
6✔
1796
                uv: uv.into(),
6✔
1797
            });
1798
        }
1799

1800
        let gpu_limits = GpuLimits::from_device(&device);
1✔
1801

1802
        // Ensure individual GpuSpawnerParams elements are properly aligned so they can
1803
        // be addressed individually by the computer shaders.
1804
        let item_align = gpu_limits.storage_buffer_align().get() as u64;
1✔
1805
        trace!(
1✔
1806
            "Aligning storage buffers to {} bytes as device limits requires.",
×
1807
            item_align
1808
        );
1809

1810
        Self {
1811
            entity_map: HashMap::default(),
1✔
1812
            view_bind_group: None,
1813
            sim_params_bind_group: None,
1814
            spawner_bind_group: None,
1815
            dr_indirect_bind_group: None,
1816
            init_render_indirect_bind_group: None,
1817
            sim_params_uniforms: UniformBuffer::default(),
1✔
1818
            spawner_buffer: AlignedBufferVec::new(
1✔
1819
                BufferUsages::STORAGE,
1820
                NonZeroU64::new(item_align),
1821
                Some("hanabi:buffer:spawner".to_string()),
1822
            ),
1823
            dispatch_indirect_buffer: BufferTable::new(
1✔
1824
                BufferUsages::STORAGE | BufferUsages::INDIRECT,
1825
                // NOTE: Technically we're using an offset in dispatch_workgroups_indirect(), but
1826
                // `min_storage_buffer_offset_alignment` is documented as being for the offset in
1827
                // BufferBinding and the dynamic offset in set_bind_group(), so either the
1828
                // documentation is lacking or we don't need to align here.
1829
                NonZeroU64::new(item_align),
1830
                Some("hanabi:buffer:dispatch_indirect".to_string()),
1831
            ),
1832
            render_effect_dispatch_buffer: BufferTable::new(
1✔
1833
                BufferUsages::STORAGE | BufferUsages::INDIRECT,
1834
                NonZeroU64::new(item_align),
1835
                Some("hanabi:buffer:render_effect_dispatch".to_string()),
1836
            ),
1837
            render_group_dispatch_buffer: BufferTable::new(
1✔
1838
                BufferUsages::STORAGE | BufferUsages::INDIRECT,
1839
                NonZeroU64::new(item_align),
1840
                Some("hanabi:buffer:render_group_dispatch".to_string()),
1841
            ),
1842
            particle_group_buffer: AlignedBufferVec::new(
1✔
1843
                BufferUsages::STORAGE,
1844
                NonZeroU64::new(item_align),
1845
                Some("hanabi:buffer:particle_group".to_string()),
1846
            ),
1847
            vertices,
1848
            indirect_dispatch_pipeline: None,
1849
            gpu_limits,
1850
        }
1851
    }
1852

1853
    /// Allocate internal resources for newly spawned effects, and deallocate
1854
    /// them for just-removed ones.
1855
    pub fn add_remove_effects(
10✔
1856
        &mut self,
1857
        mut added_effects: Vec<AddedEffect>,
1858
        removed_effect_entities: Vec<Entity>,
1859
        render_device: &RenderDevice,
1860
        render_queue: &RenderQueue,
1861
        effect_bind_groups: &mut ResMut<EffectBindGroups>,
1862
        effect_cache: &mut ResMut<EffectCache>,
1863
    ) {
1864
        // Deallocate GPU data for destroyed effect instances. This will automatically
1865
        // drop any group where there is no more effect slice.
1866
        trace!(
10✔
1867
            "Removing {} despawned effects",
×
1868
            removed_effect_entities.len()
×
1869
        );
1870
        for entity in &removed_effect_entities {
10✔
1871
            trace!("Removing ParticleEffect on entity {:?}", entity);
×
1872
            if let Some(entry) = self.entity_map.remove(entity) {
×
1873
                trace!(
1874
                    "=> ParticleEffect on entity {:?} had cache ID {:?}, removing...",
×
1875
                    entity,
1876
                    entry.cache_id
1877
                );
1878
                if let Some(cached_effect_indices) = effect_cache.remove(entry.cache_id) {
×
1879
                    // Clear bind groups associated with the removed buffer
1880
                    trace!(
1881
                        "=> GPU buffer #{} gone, destroying its bind groups...",
×
1882
                        cached_effect_indices.buffer_index
1883
                    );
1884
                    effect_bind_groups
×
1885
                        .particle_buffers
×
1886
                        .remove(&cached_effect_indices.buffer_index);
×
1887

1888
                    let slices_ref = &cached_effect_indices.slices;
×
1889
                    debug_assert!(slices_ref.ranges.len() >= 2);
×
1890
                    let group_count = (slices_ref.ranges.len() - 1) as u32;
×
1891

1892
                    let first_row = slices_ref
×
1893
                        .dispatch_buffer_indices
×
1894
                        .first_update_group_dispatch_buffer_index
×
1895
                        .0;
×
1896
                    for table_id in first_row..(first_row + group_count) {
×
1897
                        self.dispatch_indirect_buffer
×
1898
                            .remove(BufferTableId(table_id));
×
1899
                    }
1900
                    self.render_effect_dispatch_buffer.remove(
×
1901
                        slices_ref
×
1902
                            .dispatch_buffer_indices
×
1903
                            .render_effect_metadata_buffer_index,
×
1904
                    );
1905
                    let first_row = slices_ref
×
1906
                        .dispatch_buffer_indices
×
1907
                        .first_render_group_dispatch_buffer_index
×
1908
                        .0;
×
1909
                    for table_id in first_row..(first_row + group_count) {
×
1910
                        self.render_group_dispatch_buffer
×
1911
                            .remove(BufferTableId(table_id));
×
1912
                    }
1913
                }
1914
            }
1915
        }
1916

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

1923
        trace!("Adding {} newly spawned effects", added_effects.len());
10✔
1924
        for added_effect in added_effects.drain(..) {
10✔
1925
            let total_capacity = added_effect.capacities.iter().cloned().sum();
×
1926

1927
            let first_update_group_dispatch_buffer_index = allocate_sequential_buffers(
1928
                &mut self.dispatch_indirect_buffer,
×
1929
                iter::repeat(GpuDispatchIndirect::default()).take(added_effect.capacities.len()),
×
1930
            );
1931

1932
            let render_effect_dispatch_buffer_id =
×
1933
                self.render_effect_dispatch_buffer
×
1934
                    .insert(GpuRenderEffectMetadata {
×
1935
                        max_spawn: total_capacity,
×
1936
                        ..default()
×
1937
                    });
1938

1939
            let mut current_base_instance = 0;
×
1940
            let first_render_group_dispatch_buffer_index = allocate_sequential_buffers(
1941
                &mut self.render_group_dispatch_buffer,
×
1942
                added_effect.capacities.iter().map(|&capacity| {
×
1943
                    let indirect_dispatch = GpuRenderGroupIndirect {
×
1944
                        vertex_count: 6, // TODO - Flexible vertex count and mesh particles
×
1945
                        dead_count: capacity,
×
1946
                        base_instance: current_base_instance,
×
1947
                        ..default()
×
1948
                    };
1949
                    current_base_instance += capacity;
×
1950
                    indirect_dispatch
×
1951
                }),
1952
            );
1953

1954
            let dispatch_buffer_indices = DispatchBufferIndices {
1955
                first_update_group_dispatch_buffer_index,
1956
                render_effect_metadata_buffer_index: render_effect_dispatch_buffer_id,
1957
                first_render_group_dispatch_buffer_index,
1958
            };
1959

1960
            let cache_id = effect_cache.insert(
×
1961
                added_effect.handle,
×
1962
                added_effect.capacities,
×
1963
                &added_effect.particle_layout,
×
1964
                &added_effect.property_layout,
×
1965
                added_effect.layout_flags,
×
1966
                dispatch_buffer_indices,
×
1967
            );
1968

1969
            let entity = added_effect.entity;
×
1970
            self.entity_map.insert(entity, CacheEntry { cache_id });
×
1971

1972
            // Note: those effects are already in extracted_effects.effects
1973
            // because they were gathered by the same query as
1974
            // previously existing ones, during extraction.
1975

1976
            // let index = self.effect_cache.buffer_index(cache_id).unwrap();
1977
            //
1978
            // let table_id = self
1979
            // .dispatch_indirect_buffer
1980
            // .insert(GpuDispatchIndirect::default());
1981
            // assert_eq!(
1982
            // table_id.0, index,
1983
            // "Broken table invariant: buffer={} row={}",
1984
            // index, table_id.0
1985
            // );
1986
        }
1987

1988
        // Once all changes are applied, immediately schedule any GPU buffer
1989
        // (re)allocation based on the new buffer size. The actual GPU buffer content
1990
        // will be written later.
1991
        if self
10✔
1992
            .dispatch_indirect_buffer
10✔
1993
            .allocate_gpu(render_device, render_queue)
10✔
1994
        {
1995
            // All those bind groups use the buffer so need to be re-created
1996
            effect_bind_groups.particle_buffers.clear();
×
1997
        }
1998
        if self
10✔
1999
            .render_effect_dispatch_buffer
10✔
2000
            .allocate_gpu(render_device, render_queue)
10✔
2001
        {
2002
            // All those bind groups use the buffer so need to be re-created
2003
            self.dr_indirect_bind_group = None;
×
2004
            self.init_render_indirect_bind_group = None;
×
2005
            effect_bind_groups
×
2006
                .update_render_indirect_bind_groups
×
2007
                .clear();
2008
        }
2009
        if self
10✔
2010
            .render_group_dispatch_buffer
10✔
2011
            .allocate_gpu(render_device, render_queue)
10✔
2012
        {
2013
            // All those bind groups use the buffer so need to be re-created
2014
            self.dr_indirect_bind_group = None;
×
2015
            self.init_render_indirect_bind_group = None;
×
2016
            effect_bind_groups
×
2017
                .update_render_indirect_bind_groups
×
2018
                .clear();
2019
        }
2020
    }
2021
}
2022

2023
const QUAD_VERTEX_POSITIONS: &[Vec3] = &[
2024
    Vec3::from_array([-0.5, -0.5, 0.0]),
2025
    Vec3::from_array([0.5, 0.5, 0.0]),
2026
    Vec3::from_array([-0.5, 0.5, 0.0]),
2027
    Vec3::from_array([-0.5, -0.5, 0.0]),
2028
    Vec3::from_array([0.5, -0.5, 0.0]),
2029
    Vec3::from_array([0.5, 0.5, 0.0]),
2030
];
2031

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

2051
impl Default for LayoutFlags {
2052
    fn default() -> Self {
1✔
2053
        Self::NONE
1✔
2054
    }
2055
}
2056

2057
pub(crate) fn prepare_effects(
10✔
2058
    mut commands: Commands,
2059
    sim_params: Res<SimParams>,
2060
    render_device: Res<RenderDevice>,
2061
    render_queue: Res<RenderQueue>,
2062
    pipeline_cache: Res<PipelineCache>,
2063
    dispatch_indirect_pipeline: Res<DispatchIndirectPipeline>,
2064
    init_pipeline: Res<ParticlesInitPipeline>,
2065
    update_pipeline: Res<ParticlesUpdatePipeline>,
2066
    mut specialized_init_pipelines: ResMut<SpecializedComputePipelines<ParticlesInitPipeline>>,
2067
    mut specialized_update_pipelines: ResMut<SpecializedComputePipelines<ParticlesUpdatePipeline>>,
2068
    // update_pipeline: Res<ParticlesUpdatePipeline>, // TODO move update_pipeline.pipeline to
2069
    // EffectsMeta
2070
    mut effects_meta: ResMut<EffectsMeta>,
2071
    mut effect_cache: ResMut<EffectCache>,
2072
    mut extracted_effects: ResMut<ExtractedEffects>,
2073
    mut effect_bind_groups: ResMut<EffectBindGroups>,
2074
) {
2075
    trace!("prepare_effects");
10✔
2076

2077
    // Allocate spawner buffer if needed
2078
    // if effects_meta.spawner_buffer.is_empty() {
2079
    //    effects_meta.spawner_buffer.push(GpuSpawnerParams::default());
2080
    //}
2081

2082
    // Write vertices (TODO - lazily once only)
2083
    effects_meta
10✔
2084
        .vertices
10✔
2085
        .write_buffer(&render_device, &render_queue);
10✔
2086

2087
    effects_meta.indirect_dispatch_pipeline = Some(dispatch_indirect_pipeline.pipeline.clone());
10✔
2088

2089
    // Clear last frame's buffer resizes which may have occured during last frame,
2090
    // during `Node::run()` while the `BufferTable` could not be mutated.
2091
    effects_meta
10✔
2092
        .dispatch_indirect_buffer
10✔
2093
        .clear_previous_frame_resizes();
2094
    effects_meta
10✔
2095
        .render_effect_dispatch_buffer
10✔
2096
        .clear_previous_frame_resizes();
2097
    effects_meta
10✔
2098
        .render_group_dispatch_buffer
10✔
2099
        .clear_previous_frame_resizes();
2100

2101
    // Allocate new effects, deallocate removed ones
2102
    let removed_effect_entities = std::mem::take(&mut extracted_effects.removed_effect_entities);
10✔
2103
    for entity in &removed_effect_entities {
10✔
2104
        extracted_effects.effects.remove(entity);
×
2105
    }
2106
    effects_meta.add_remove_effects(
2107
        std::mem::take(&mut extracted_effects.added_effects),
2108
        removed_effect_entities,
2109
        &render_device,
2110
        &render_queue,
2111
        &mut effect_bind_groups,
2112
        &mut effect_cache,
2113
    );
2114

2115
    // // sort first by z and then by handle. this ensures that, when possible,
2116
    // batches span multiple z layers // batches won't span z-layers if there is
2117
    // another batch between them extracted_effects.effects.sort_by(|a, b| {
2118
    //     match FloatOrd(a.transform.w_axis[2]).cmp(&FloatOrd(b.transform.
2119
    // w_axis[2])) {         Ordering::Equal => a.handle.cmp(&b.handle),
2120
    //         other => other,
2121
    //     }
2122
    // });
2123

2124
    // Build batcher inputs from extracted effects
2125
    let effects = std::mem::take(&mut extracted_effects.effects);
2126

2127
    let effect_entity_list = effects
2128
        .into_iter()
2129
        .map(|(entity, extracted_effect)| {
×
2130
            let id = effects_meta.entity_map.get(&entity).unwrap().cache_id;
×
2131
            let property_buffer = effect_cache.get_property_buffer(id).cloned(); // clone handle for lifetime
×
2132
            let effect_slices = effect_cache.get_slices(id);
×
2133

2134
            BatchesInput {
×
2135
                handle: extracted_effect.handle,
×
2136
                entity,
×
2137
                effect_slices,
×
2138
                property_layout: extracted_effect.property_layout.clone(),
×
2139
                effect_shader: extracted_effect.effect_shader.clone(),
×
2140
                layout_flags: extracted_effect.layout_flags,
×
NEW
2141
                texture_layout: extracted_effect.texture_layout.clone(),
×
NEW
2142
                textures: extracted_effect.textures.clone(),
×
2143
                alpha_mode: extracted_effect.alpha_mode,
×
2144
                spawn_count: extracted_effect.spawn_count,
×
2145
                transform: extracted_effect.transform.into(),
×
2146
                inverse_transform: extracted_effect.inverse_transform.into(),
×
2147
                property_buffer,
×
2148
                property_data: extracted_effect.property_data,
×
2149
                #[cfg(feature = "2d")]
×
2150
                z_sort_key_2d: extracted_effect.z_sort_key_2d,
×
2151
            }
2152
        })
2153
        .collect::<Vec<_>>();
2154
    trace!("Collected {} extracted effects", effect_entity_list.len());
×
2155

2156
    // Sort first by effect buffer index, then by slice range (see EffectSlice)
2157
    // inside that buffer. This is critical for batching to work, because
2158
    // batching effects is based on compatible items, which implies same GPU
2159
    // buffer and continuous slice ranges (the next slice start must be equal to
2160
    // the previous start end, without gap). EffectSlice already contains both
2161
    // information, and the proper ordering implementation.
2162
    // effect_entity_list.sort_by_key(|a| a.effect_slice.clone());
2163

2164
    // Loop on all extracted effects in order and try to batch them together to
2165
    // reduce draw calls
2166
    effects_meta.spawner_buffer.clear();
10✔
2167
    effects_meta.particle_group_buffer.clear();
10✔
2168
    let mut total_group_count = 0;
10✔
2169
    for (effect_index, input) in effect_entity_list.into_iter().enumerate() {
×
2170
        // Specialize the init pipeline based on the effect. Note that this is shared by
2171
        // all effect groups of a same effect.
2172
        trace!(
×
2173
            "Specializing compute pipeline: init_shader={:?} particle_layout={:?}",
×
2174
            input.effect_shader.init,
2175
            input.effect_slices.particle_layout
2176
        );
2177
        let init_pipeline_id = specialized_init_pipelines.specialize(
×
2178
            &pipeline_cache,
2179
            &init_pipeline,
2180
            ParticleInitPipelineKey {
2181
                shader: input.effect_shader.init.clone(),
2182
                particle_layout_min_binding_size: input
2183
                    .effect_slices
2184
                    .particle_layout
2185
                    .min_binding_size(),
2186
                property_layout_min_binding_size: if input.property_layout.is_empty() {
2187
                    None
×
2188
                } else {
2189
                    Some(input.property_layout.min_binding_size())
×
2190
                },
2191
            },
2192
        );
2193
        trace!("Init pipeline specialized: id={:?}", init_pipeline_id);
×
2194

2195
        // Specialize the update pipelines based on the effect
2196
        trace!(
×
2197
            "Specializing update pipeline(s): update_shader(s)={:?} particle_layout={:?} property_layout={:?}",
×
2198
            input.effect_shader.update,
2199
            input.effect_slices.particle_layout,
2200
            input.property_layout,
2201
        );
2202
        let update_pipeline_ids: Vec<_> = input
×
2203
            .effect_shader
×
2204
            .update
×
2205
            .iter()
2206
            .map(|update_source| {
×
2207
                specialized_update_pipelines.specialize(
×
2208
                    &pipeline_cache,
×
2209
                    &update_pipeline,
×
2210
                    ParticleUpdatePipelineKey {
×
2211
                        shader: update_source.clone(),
×
2212
                        particle_layout: input.effect_slices.particle_layout.clone(),
×
2213
                        property_layout: input.property_layout.clone(),
×
2214
                    },
2215
                )
2216
            })
2217
            .collect();
2218
        trace!(
2219
            "Update pipeline(s) specialized: ids={:?}",
×
2220
            update_pipeline_ids
2221
        );
2222

2223
        let init_shader = input.effect_shader.init.clone();
×
2224
        trace!("init_shader = {:?}", init_shader);
×
2225

2226
        let update_shader = input.effect_shader.update.clone();
×
2227
        trace!("update_shader(s) = {:?}", update_shader);
×
2228

2229
        let render_shader = input.effect_shader.render.clone();
×
2230
        trace!("render_shader(s) = {:?}", render_shader);
×
2231

2232
        let layout_flags = input.layout_flags;
×
UNCOV
2233
        trace!("layout_flags = {:?}", layout_flags);
×
2234

2235
        trace!(
×
2236
            "particle_layout = {:?}",
×
2237
            input.effect_slices.particle_layout
2238
        );
2239

2240
        #[cfg(feature = "2d")]
2241
        {
2242
            trace!("z_sort_key_2d = {:?}", input.z_sort_key_2d);
×
2243
        }
2244

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

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

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

2285
        let effect_cache_id = effects_meta.entity_map.get(&input.entity).unwrap().cache_id;
×
2286
        let dispatch_buffer_indices = effect_cache.get_dispatch_buffer_indices(effect_cache_id);
×
2287

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

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

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

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

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

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

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

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

2357
        gpu_sim_params.num_groups = total_group_count;
10✔
2358

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

2585
            let alpha_mode = batches.alpha_mode;
×
2586

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

2759
            let alpha_mode = batches.alpha_mode;
×
2760

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

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

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

2834
    trace!("queue_effects");
×
2835

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

3078
        // Create the bind group for the indirect dispatch of all effects
3079
        effects_meta.dr_indirect_bind_group = Some(render_device.create_bind_group(
×
3080
            "hanabi:bind_group_vfx_indirect_dr_indirect",
×
3081
            &dispatch_indirect_pipeline.dispatch_indirect_layout,
×
3082
            &[
×
3083
                BindGroupEntry {
×
3084
                    binding: 0,
×
3085
                    resource: BindingResource::Buffer(BufferBinding {
×
3086
                        buffer: effects_meta.render_effect_dispatch_buffer.buffer().unwrap(),
×
3087
                        offset: 0,
×
3088
                        size: None, //NonZeroU64::new(256), // Some(GpuRenderIndirect::min_size()),
×
3089
                    }),
3090
                },
3091
                BindGroupEntry {
×
3092
                    binding: 1,
×
3093
                    resource: BindingResource::Buffer(BufferBinding {
×
3094
                        buffer: effects_meta.render_group_dispatch_buffer.buffer().unwrap(),
×
3095
                        offset: 0,
×
3096
                        size: None, //NonZeroU64::new(256), // Some(GpuRenderIndirect::min_size()),
×
3097
                    }),
3098
                },
3099
                BindGroupEntry {
×
3100
                    binding: 2,
×
3101
                    resource: BindingResource::Buffer(BufferBinding {
×
3102
                        buffer: effects_meta.dispatch_indirect_buffer.buffer().unwrap(),
×
3103
                        offset: 0,
×
3104
                        size: None, //NonZeroU64::new(256), // Some(GpuDispatchIndirect::min_size()),
×
3105
                    }),
3106
                },
3107
                BindGroupEntry {
×
3108
                    binding: 3,
×
3109
                    resource: BindingResource::Buffer(BufferBinding {
×
3110
                        buffer: effects_meta.particle_group_buffer.buffer().unwrap(),
×
3111
                        offset: 0,
×
3112
                        size: None,
×
3113
                    }),
3114
                },
3115
            ],
3116
        ));
3117

3118
        // Create the bind group for the indirect render buffer use in the init shader
3119
        effects_meta.init_render_indirect_bind_group = Some(render_device.create_bind_group(
×
3120
            "hanabi:bind_group_init_render_dispatch",
×
3121
            &init_pipeline.render_indirect_layout,
×
3122
            &[
×
3123
                BindGroupEntry {
×
3124
                    binding: 0,
×
3125
                    resource: BindingResource::Buffer(BufferBinding {
×
3126
                        buffer: effects_meta.render_effect_dispatch_buffer.buffer().unwrap(),
×
3127
                        offset: 0,
×
3128
                        size: Some(effects_meta.gpu_limits.render_effect_indirect_size()),
×
3129
                    }),
3130
                },
3131
                BindGroupEntry {
×
3132
                    binding: 1,
×
3133
                    resource: BindingResource::Buffer(BufferBinding {
×
3134
                        buffer: effects_meta.render_group_dispatch_buffer.buffer().unwrap(),
×
3135
                        // Always bind the first array element of the buffer, corresponding to the
3136
                        // first effect group, as only the first group has an init pass.
3137
                        offset: 0,
×
3138
                        size: Some(effects_meta.gpu_limits.render_group_indirect_size()),
×
3139
                    }),
3140
                },
3141
            ],
3142
        ));
3143
    }
3144

3145
    // Make a copy of the buffer ID before borrowing effects_meta mutably in the
3146
    // loop below
3147
    let indirect_buffer = effects_meta
×
3148
        .dispatch_indirect_buffer
×
3149
        .buffer()
3150
        .cloned()
3151
        .unwrap();
3152
    let spawner_buffer = effects_meta.spawner_buffer.buffer().cloned().unwrap();
×
3153

3154
    // Create the per-effect render bind groups
3155
    trace!("Create per-effect render bind groups...");
×
3156
    for (buffer_index, buffer) in effect_cache.buffers().iter().enumerate() {
×
3157
        #[cfg(feature = "trace")]
3158
        let _span_buffer = bevy::utils::tracing::info_span!("create_buffer_bind_groups").entered();
×
3159

3160
        let Some(buffer) = buffer else {
×
3161
            trace!(
×
3162
                "Effect buffer index #{} has no allocated EffectBuffer, skipped.",
×
3163
                buffer_index
3164
            );
3165
            continue;
×
3166
        };
3167

3168
        // Ensure all effect groups have a bind group for the entire buffer of the
3169
        // group, since the update phase runs on an entire group/buffer at once,
3170
        // with all the effect instances in it batched together.
3171
        trace!("effect particle buffer_index=#{}", buffer_index);
×
3172
        effect_bind_groups
×
3173
            .particle_buffers
×
3174
            .entry(buffer_index as u32)
×
3175
            .or_insert_with(|| {
×
3176
                trace!(
×
3177
                    "Create new particle bind groups for buffer_index={} | particle_layout {:?} | property_layout {:?}",
×
3178
                    buffer_index,
×
3179
                    buffer.particle_layout(),
×
3180
                    buffer.property_layout(),
×
3181
                );
3182

3183
                let dispatch_indirect_size = GpuDispatchIndirect::aligned_size(render_device
×
3184
                    .limits()
×
3185
                    .min_storage_buffer_offset_alignment);
×
3186
                let mut entries = vec![
×
3187
                    BindGroupEntry {
×
3188
                        binding: 0,
×
3189
                        resource: buffer.max_binding(),
×
3190
                    },
3191
                    BindGroupEntry {
×
3192
                        binding: 1,
×
3193
                        resource: buffer.indirect_max_binding(),
×
3194
                    },
3195
                    BindGroupEntry {
×
3196
                        binding: 2,
×
3197
                        resource: BindingResource::Buffer(BufferBinding {
×
3198
                            buffer: &indirect_buffer,
×
3199
                            offset: 0,
×
3200
                            size: Some(dispatch_indirect_size),
×
3201
                        }),
3202
                    },
3203
                ];
3204
                if buffer.layout_flags().contains(LayoutFlags::LOCAL_SPACE_SIMULATION) {
×
3205
                    entries.push(BindGroupEntry {
×
3206
                        binding: 3,
×
3207
                        resource: BindingResource::Buffer(BufferBinding {
×
3208
                            buffer: &spawner_buffer,
×
3209
                            offset: 0,
×
3210
                            size: Some(GpuSpawnerParams::min_size()),
×
3211
                        }),
3212
                    });
3213
                }
3214
                trace!("Creating render bind group with {} entries (layout flags: {:?})", entries.len(), buffer.layout_flags());
×
3215
                let render = render_device.create_bind_group(
×
3216
                    &format!("hanabi:bind_group_render_vfx{buffer_index}_particles")[..],
×
3217
                     buffer.particle_layout_bind_group_with_dispatch(),
×
3218
                     &entries,
×
3219
                );
3220

3221
                BufferBindGroups {
×
3222
                    render,
×
3223
                }
3224
            });
3225
    }
3226

3227
    // Create the per-effect bind groups.
3228
    for (entity, effect_batches) in effect_batches.iter() {
×
3229
        #[cfg(feature = "trace")]
3230
        let _span_buffer = bevy::utils::tracing::info_span!("create_batch_bind_groups").entered();
×
3231

3232
        let effect_cache_id = effect_batches.effect_cache_id;
3233

3234
        // Convert indirect buffer offsets from indices to bytes.
3235
        let first_effect_particle_group_buffer_offset = effects_meta
3236
            .gpu_limits
3237
            .particle_group_offset(effect_batches.first_particle_group_buffer_index)
3238
            as u64;
3239
        let effect_particle_groups_buffer_size = NonZeroU64::try_from(
3240
            u32::from(effects_meta.gpu_limits.particle_group_aligned_size) as u64
3241
                * effect_batches.group_batches.len() as u64,
3242
        )
3243
        .unwrap();
3244
        let group_binding = BufferBinding {
3245
            buffer: effects_meta.particle_group_buffer.buffer().unwrap(),
3246
            offset: first_effect_particle_group_buffer_offset,
3247
            size: Some(effect_particle_groups_buffer_size),
3248
        };
3249

3250
        let Some(Some(effect_buffer)) = effect_cache
×
3251
            .buffers_mut()
3252
            .get_mut(effect_batches.buffer_index as usize)
3253
        else {
3254
            error!("No particle buffer allocated for entity {:?}", entity);
×
3255
            continue;
×
3256
        };
3257

3258
        // Bind group for the init compute shader to simulate particles.
3259
        // TODO - move this creation in RenderSet::PrepareBindGroups
3260
        effect_buffer.create_sim_bind_group(
×
3261
            effect_batches.buffer_index,
×
3262
            &render_device,
×
3263
            group_binding,
×
3264
        );
3265

3266
        if effect_bind_groups
×
3267
            .update_render_indirect_bind_groups
×
3268
            .get(&effect_cache_id)
×
3269
            .is_none()
3270
        {
3271
            let DispatchBufferIndices {
×
3272
                render_effect_metadata_buffer_index: render_effect_dispatch_buffer_index,
×
3273
                first_render_group_dispatch_buffer_index,
×
3274
                ..
×
3275
            } = effect_batches.dispatch_buffer_indices;
×
3276

3277
            let storage_alignment = effects_meta.gpu_limits.storage_buffer_align.get();
×
3278
            let render_effect_indirect_size =
×
3279
                GpuRenderEffectMetadata::aligned_size(storage_alignment);
×
3280
            let total_render_group_indirect_size = NonZeroU64::new(
3281
                GpuRenderGroupIndirect::aligned_size(storage_alignment).get()
×
3282
                    * effect_batches.group_batches.len() as u64,
×
3283
            )
3284
            .unwrap();
3285
            let particles_buffer_layout_update_render_indirect = render_device.create_bind_group(
×
3286
                "hanabi:bind_group_update_render_group_dispatch",
3287
                &update_pipeline.render_indirect_layout,
×
3288
                &[
×
3289
                    BindGroupEntry {
×
3290
                        binding: 0,
×
3291
                        resource: BindingResource::Buffer(BufferBinding {
×
3292
                            buffer: effects_meta.render_effect_dispatch_buffer.buffer().unwrap(),
×
3293
                            offset: effects_meta.gpu_limits.render_effect_indirect_offset(
×
3294
                                render_effect_dispatch_buffer_index.0,
×
3295
                            ),
3296
                            size: Some(render_effect_indirect_size),
×
3297
                        }),
3298
                    },
3299
                    BindGroupEntry {
×
3300
                        binding: 1,
×
3301
                        resource: BindingResource::Buffer(BufferBinding {
×
3302
                            buffer: effects_meta.render_group_dispatch_buffer.buffer().unwrap(),
×
3303
                            offset: effects_meta.gpu_limits.render_group_indirect_offset(
×
3304
                                first_render_group_dispatch_buffer_index.0,
×
3305
                            ),
3306
                            size: Some(total_render_group_indirect_size),
×
3307
                        }),
3308
                    },
3309
                ],
3310
            );
3311

3312
            effect_bind_groups
×
3313
                .update_render_indirect_bind_groups
×
3314
                .insert(
3315
                    effect_cache_id,
×
3316
                    particles_buffer_layout_update_render_indirect,
×
3317
                );
3318
        }
3319

3320
        // Ensure the particle texture(s) are available as GPU resources and that a bind
3321
        // group for them exists FIXME fix this insert+get below
NEW
3322
        if !effect_batches.texture_layout.layout.is_empty() {
×
3323
            // This should always be available, as this is cached into the render pipeline
3324
            // just before we start specializing it.
NEW
3325
            let Some(material_bind_group_layout) =
×
NEW
3326
                render_pipeline.get_material(&effect_batches.texture_layout)
×
3327
            else {
NEW
3328
                error!(
×
NEW
3329
                    "Failed to find material bind group layout for buffer #{}",
×
3330
                    effect_batches.buffer_index
3331
                );
NEW
3332
                continue;
×
3333
            };
3334

3335
            // TODO = move
3336
            let material = Material {
3337
                layout: effect_batches.texture_layout.clone(),
NEW
3338
                textures: effect_batches.textures.iter().map(|h| h.id()).collect(),
×
3339
            };
3340
            assert_eq!(material.layout.layout.len(), material.textures.len());
3341

NEW
3342
            let bind_group_entries = material.make_entries(&gpu_images);
×
3343

NEW
3344
            effect_bind_groups
×
NEW
3345
                .material_bind_groups
×
NEW
3346
                .entry(material.clone())
×
NEW
3347
                .or_insert_with(|| {
×
NEW
3348
                    render_device.create_bind_group(
×
NEW
3349
                        &format!(
×
NEW
3350
                            "hanabi:material_bind_group_{}",
×
NEW
3351
                            material.layout.layout.len()
×
NEW
3352
                        )[..],
×
NEW
3353
                        material_bind_group_layout,
×
NEW
3354
                        &bind_group_entries[..],
×
3355
                    )
3356
                });
3357
        }
3358
    }
3359
}
3360

3361
type DrawEffectsSystemState = SystemState<(
3362
    SRes<EffectsMeta>,
3363
    SRes<EffectBindGroups>,
3364
    SRes<PipelineCache>,
3365
    SQuery<Read<ViewUniformOffset>>,
3366
    SQuery<Read<EffectBatches>>,
3367
    SQuery<Read<EffectDrawBatch>>,
3368
)>;
3369

3370
/// Draw function for rendering all active effects for the current frame.
3371
///
3372
/// Effects are rendered in the [`Transparent2d`] phase of the main 2D pass,
3373
/// and the [`Transparent3d`] phase of the main 3D pass.
3374
pub(crate) struct DrawEffects {
3375
    params: DrawEffectsSystemState,
3376
}
3377

3378
impl DrawEffects {
3379
    pub fn new(world: &mut World) -> Self {
3✔
3380
        Self {
3381
            params: SystemState::new(world),
3✔
3382
        }
3383
    }
3384
}
3385

3386
/// Draw all particles of a single effect in view, in 2D or 3D.
3387
///
3388
/// FIXME: use pipeline ID to look up which group index it is.
3389
fn draw<'w>(
×
3390
    world: &'w World,
3391
    pass: &mut TrackedRenderPass<'w>,
3392
    view: Entity,
3393
    entity: Entity,
3394
    pipeline_id: CachedRenderPipelineId,
3395
    params: &mut DrawEffectsSystemState,
3396
) {
3397
    let (effects_meta, effect_bind_groups, pipeline_cache, views, effects, effect_draw_batches) =
×
3398
        params.get(world);
×
3399
    let view_uniform = views.get(view).unwrap();
×
3400
    let effects_meta = effects_meta.into_inner();
×
3401
    let effect_bind_groups = effect_bind_groups.into_inner();
×
3402
    let effect_draw_batch = effect_draw_batches.get(entity).unwrap();
×
3403
    let effect_batches = effects.get(effect_draw_batch.batches_entity).unwrap();
×
3404

3405
    let gpu_limits = &effects_meta.gpu_limits;
×
3406

3407
    let Some(pipeline) = pipeline_cache.into_inner().get_render_pipeline(pipeline_id) else {
×
3408
        return;
×
3409
    };
3410

3411
    trace!("render pass");
×
3412

3413
    pass.set_render_pipeline(pipeline);
×
3414

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

3418
    // View properties (camera matrix, etc.)
3419
    pass.set_bind_group(
×
3420
        0,
3421
        effects_meta.view_bind_group.as_ref().unwrap(),
×
3422
        &[view_uniform.offset],
×
3423
    );
3424

3425
    // Particles buffer
3426
    let dispatch_indirect_offset = gpu_limits.dispatch_indirect_offset(effect_batches.buffer_index);
×
3427
    trace!(
×
3428
        "set_bind_group(1): dispatch_indirect_offset={}",
×
3429
        dispatch_indirect_offset
×
3430
    );
3431
    let spawner_base = effect_batches.spawner_base;
×
3432
    let spawner_buffer_aligned = effects_meta.spawner_buffer.aligned_size();
×
3433
    assert!(spawner_buffer_aligned >= GpuSpawnerParams::min_size().get() as usize);
×
3434
    let spawner_offset = spawner_base * spawner_buffer_aligned as u32;
×
3435
    let dyn_uniform_indices: [u32; 2] = [dispatch_indirect_offset, spawner_offset];
×
3436
    let dyn_uniform_indices = if effect_batches
×
3437
        .layout_flags
×
3438
        .contains(LayoutFlags::LOCAL_SPACE_SIMULATION)
×
3439
    {
3440
        &dyn_uniform_indices
×
3441
    } else {
3442
        &dyn_uniform_indices[..1]
×
3443
    };
3444
    pass.set_bind_group(
×
3445
        1,
3446
        effect_bind_groups
×
3447
            .particle_render(effect_batches.buffer_index)
×
3448
            .unwrap(),
×
3449
        dyn_uniform_indices,
×
3450
    );
3451

3452
    // Particle texture
3453
    // TODO = move
3454
    let material = Material {
NEW
3455
        layout: effect_batches.texture_layout.clone(),
×
NEW
3456
        textures: effect_batches.textures.iter().map(|h| h.id()).collect(),
×
3457
    };
NEW
3458
    if !effect_batches.texture_layout.layout.is_empty() {
×
NEW
3459
        if let Some(bind_group) = effect_bind_groups.material_bind_groups.get(&material) {
×
UNCOV
3460
            pass.set_bind_group(2, bind_group, &[]);
×
3461
        } else {
3462
            // Texture(s) not ready; skip this drawing for now
3463
            trace!(
×
NEW
3464
                "Particle material bind group not available for batch buf={}. Skipping draw call.",
×
3465
                effect_batches.buffer_index,
×
3466
            );
3467
            return; // continue;
×
3468
        }
3469
    }
3470

3471
    let render_indirect_buffer = effects_meta.render_group_dispatch_buffer.buffer().unwrap();
×
3472
    let group_index = effect_draw_batch.group_index;
×
3473
    let effect_batch = &effect_batches.group_batches[group_index as usize];
×
3474

3475
    let render_group_dispatch_indirect_index = effect_batches
×
3476
        .dispatch_buffer_indices
×
3477
        .first_render_group_dispatch_buffer_index
×
3478
        .0
×
3479
        + group_index;
×
3480

3481
    trace!(
×
3482
        "Draw up to {} particles with {} vertices per particle for batch from buffer #{} \
×
3483
            (render_group_dispatch_indirect_index={:?}, group_index={}).",
×
3484
        effect_batch.slice.len(),
×
3485
        effects_meta.vertices.len(),
×
3486
        effect_batches.buffer_index,
×
3487
        render_group_dispatch_indirect_index,
×
3488
        group_index,
×
3489
    );
3490

3491
    pass.draw_indirect(
×
3492
        render_indirect_buffer,
×
3493
        render_group_dispatch_indirect_index as u64
×
3494
            * u32::from(gpu_limits.render_group_indirect_aligned_size) as u64,
×
3495
    );
3496
}
3497

3498
#[cfg(feature = "2d")]
3499
impl Draw<Transparent2d> for DrawEffects {
3500
    fn draw<'w>(
×
3501
        &mut self,
3502
        world: &'w World,
3503
        pass: &mut TrackedRenderPass<'w>,
3504
        view: Entity,
3505
        item: &Transparent2d,
3506
    ) {
3507
        trace!("Draw<Transparent2d>: view={:?}", view);
×
3508
        draw(
3509
            world,
×
3510
            pass,
×
3511
            view,
×
3512
            item.entity,
×
3513
            item.pipeline,
×
3514
            &mut self.params,
×
3515
        );
3516
    }
3517
}
3518

3519
#[cfg(feature = "3d")]
3520
impl Draw<Transparent3d> for DrawEffects {
3521
    fn draw<'w>(
×
3522
        &mut self,
3523
        world: &'w World,
3524
        pass: &mut TrackedRenderPass<'w>,
3525
        view: Entity,
3526
        item: &Transparent3d,
3527
    ) {
3528
        trace!("Draw<Transparent3d>: view={:?}", view);
×
3529
        draw(
3530
            world,
×
3531
            pass,
×
3532
            view,
×
3533
            item.entity,
×
3534
            item.pipeline,
×
3535
            &mut self.params,
×
3536
        );
3537
    }
3538
}
3539

3540
#[cfg(feature = "3d")]
3541
impl Draw<AlphaMask3d> for DrawEffects {
3542
    fn draw<'w>(
×
3543
        &mut self,
3544
        world: &'w World,
3545
        pass: &mut TrackedRenderPass<'w>,
3546
        view: Entity,
3547
        item: &AlphaMask3d,
3548
    ) {
3549
        trace!("Draw<AlphaMask3d>: view={:?}", view);
×
3550
        draw(
3551
            world,
×
3552
            pass,
×
3553
            view,
×
3554
            item.representative_entity,
×
3555
            item.key.pipeline,
×
3556
            &mut self.params,
×
3557
        );
3558
    }
3559
}
3560

3561
/// Render node to run the simulation sub-graph once per frame.
3562
///
3563
/// This node doesn't simulate anything by itself, but instead schedules the
3564
/// simulation sub-graph, where other nodes like [`VfxSimulateNode`] do the
3565
/// actual simulation.
3566
///
3567
/// The simulation sub-graph is scheduled to run before the [`CameraDriverNode`]
3568
/// renders all the views, such that rendered views have access to the
3569
/// just-simulated particles to render them.
3570
///
3571
/// [`CameraDriverNode`]: bevy::render::camera::CameraDriverNode
3572
pub(crate) struct VfxSimulateDriverNode;
3573

3574
impl Node for VfxSimulateDriverNode {
3575
    fn run(
10✔
3576
        &self,
3577
        graph: &mut RenderGraphContext,
3578
        _render_context: &mut RenderContext,
3579
        _world: &World,
3580
    ) -> Result<(), NodeRunError> {
3581
        graph.run_sub_graph(
10✔
3582
            crate::plugin::simulate_graph::HanabiSimulateGraph,
10✔
3583
            vec![],
10✔
3584
            None,
10✔
3585
        )?;
3586
        Ok(())
10✔
3587
    }
3588
}
3589

3590
/// Render node to run the simulation of all effects once per frame.
3591
///
3592
/// Runs inside the simulation sub-graph, looping over all extracted effect
3593
/// batches to simulate them.
3594
pub(crate) struct VfxSimulateNode {
3595
    /// Query to retrieve the batches of effects to simulate and render.
3596
    effect_query: QueryState<(Entity, Read<EffectBatches>)>,
3597
}
3598

3599
impl VfxSimulateNode {
3600
    /// Output particle buffer for that view. TODO - how to handle multiple
3601
    /// buffers?! Should use Entity instead??
3602
    // pub const OUT_PARTICLE_BUFFER: &'static str = "particle_buffer";
3603

3604
    /// Create a new node for simulating the effects of the given world.
3605
    pub fn new(world: &mut World) -> Self {
1✔
3606
        Self {
3607
            effect_query: QueryState::new(world),
1✔
3608
        }
3609
    }
3610
}
3611

3612
impl Node for VfxSimulateNode {
3613
    fn input(&self) -> Vec<SlotInfo> {
1✔
3614
        vec![]
1✔
3615
    }
3616

3617
    fn update(&mut self, world: &mut World) {
10✔
3618
        trace!("VfxSimulateNode::update()");
10✔
3619
        self.effect_query.update_archetypes(world);
10✔
3620
    }
3621

3622
    fn run(
10✔
3623
        &self,
3624
        _graph: &mut RenderGraphContext,
3625
        render_context: &mut RenderContext,
3626
        world: &World,
3627
    ) -> Result<(), NodeRunError> {
3628
        trace!("VfxSimulateNode::run()");
10✔
3629

3630
        // Get the Entity containing the ViewEffectsEntity component used as container
3631
        // for the input data for this node.
3632
        // let view_entity = graph.get_input_entity(Self::IN_VIEW)?;
3633
        let pipeline_cache = world.resource::<PipelineCache>();
10✔
3634

3635
        let effects_meta = world.resource::<EffectsMeta>();
10✔
3636
        let effect_cache = world.resource::<EffectCache>();
10✔
3637
        let effect_bind_groups = world.resource::<EffectBindGroups>();
10✔
3638
        // let render_queue = world.resource::<RenderQueue>();
3639

3640
        // Make sure to schedule any buffer copy from changed effects before accessing
3641
        // them
3642
        effects_meta
10✔
3643
            .dispatch_indirect_buffer
10✔
3644
            .write_buffer(render_context.command_encoder());
10✔
3645
        effects_meta
10✔
3646
            .render_effect_dispatch_buffer
10✔
3647
            .write_buffer(render_context.command_encoder());
10✔
3648
        effects_meta
10✔
3649
            .render_group_dispatch_buffer
10✔
3650
            .write_buffer(render_context.command_encoder());
10✔
3651

3652
        // Compute init pass
3653
        // let mut total_group_count = 0;
3654
        {
3655
            let mut compute_pass =
10✔
3656
                render_context
10✔
3657
                    .command_encoder()
3658
                    .begin_compute_pass(&ComputePassDescriptor {
10✔
3659
                        label: Some("hanabi:init"),
10✔
3660
                        timestamp_writes: None,
10✔
3661
                    });
3662

3663
            {
3664
                trace!("loop over effect batches...");
10✔
3665

3666
                // Dispatch init compute jobs
3667
                for (entity, batches) in self.effect_query.iter_manual(world) {
10✔
3668
                    let render_effect_dispatch_buffer_index = batches
×
3669
                        .dispatch_buffer_indices
×
3670
                        .render_effect_metadata_buffer_index;
×
3671
                    let first_render_group_dispatch_buffer_index = &batches
×
3672
                        .dispatch_buffer_indices
×
3673
                        .first_render_group_dispatch_buffer_index;
×
3674

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

3680
                    let Some(init_pipeline) =
×
3681
                        pipeline_cache.get_compute_pipeline(batches.init_pipeline_id)
×
3682
                    else {
3683
                        if let CachedPipelineState::Err(err) =
×
3684
                            pipeline_cache.get_compute_pipeline_state(batches.init_pipeline_id)
×
3685
                        {
3686
                            error!(
3687
                                "Failed to find init pipeline #{} for effect {:?}: {:?}",
×
3688
                                batches.init_pipeline_id.id(),
×
3689
                                entity,
3690
                                err
3691
                            );
3692
                        }
3693
                        continue;
×
3694
                    };
3695

3696
                    // Do not dispatch any init work if there's nothing to spawn this frame
3697
                    let spawn_count = batches.spawn_count;
3698
                    if spawn_count == 0 {
3699
                        continue;
×
3700
                    }
3701

3702
                    const WORKGROUP_SIZE: u32 = 64;
3703
                    let workgroup_count = (spawn_count + WORKGROUP_SIZE - 1) / WORKGROUP_SIZE;
×
3704

3705
                    let effect_cache_id = batches.effect_cache_id;
×
3706

3707
                    // for (effect_entity, effect_slice) in effects_meta.entity_map.iter() {
3708
                    // Retrieve the ExtractedEffect from the entity
3709
                    // trace!("effect_entity={:?} effect_slice={:?}", effect_entity,
3710
                    // effect_slice); let effect =
3711
                    // self.effect_query.get_manual(world, *effect_entity).unwrap();
3712

3713
                    // Get the slice to init
3714
                    // let effect_slice = effects_meta.get(&effect_entity);
3715
                    // let effect_group =
3716
                    //     &effects_meta.effect_cache.buffers()[batch.buffer_index as usize];
3717
                    let Some(particles_init_bind_group) =
×
3718
                        effect_cache.init_bind_group(effect_cache_id)
3719
                    else {
3720
                        error!(
×
3721
                            "Failed to find init particle buffer bind group for entity {:?}",
×
3722
                            entity
3723
                        );
3724
                        continue;
×
3725
                    };
3726

3727
                    let spawner_base = batches.spawner_base;
3728

3729
                    let spawner_buffer_aligned = effects_meta.spawner_buffer.aligned_size();
3730
                    assert!(spawner_buffer_aligned >= GpuSpawnerParams::min_size().get() as usize);
3731
                    let spawner_offset = spawner_base * spawner_buffer_aligned as u32;
×
3732

3733
                    let render_effect_indirect_offset = effects_meta
×
3734
                        .gpu_limits
×
3735
                        .render_effect_indirect_offset(render_effect_dispatch_buffer_index.0);
×
3736

3737
                    let first_render_group_indirect_offset = effects_meta
×
3738
                        .gpu_limits
×
3739
                        .render_group_indirect_offset(first_render_group_dispatch_buffer_index.0);
×
3740

3741
                    trace!(
×
3742
                        "record commands for init pipeline of effect {:?} \
×
3743
                            (spawn {} = {} workgroups) spawner_base={} \
×
3744
                            spawner_offset={} \
×
3745
                            render_effect_indirect_offset={} \
×
3746
                            first_render_group_indirect_offset={}...",
×
3747
                        batches.handle,
3748
                        spawn_count,
3749
                        workgroup_count,
3750
                        spawner_base,
3751
                        spawner_offset,
3752
                        render_effect_indirect_offset,
3753
                        first_render_group_indirect_offset,
3754
                    );
3755

3756
                    // Setup compute pass
3757
                    compute_pass.set_pipeline(init_pipeline);
×
3758
                    compute_pass.set_bind_group(
×
3759
                        0,
3760
                        effects_meta.sim_params_bind_group.as_ref().unwrap(),
×
3761
                        &[],
×
3762
                    );
3763
                    compute_pass.set_bind_group(1, particles_init_bind_group, &[]);
×
3764
                    compute_pass.set_bind_group(
×
3765
                        2,
3766
                        effects_meta.spawner_bind_group.as_ref().unwrap(),
×
3767
                        &[spawner_offset],
×
3768
                    );
3769
                    compute_pass.set_bind_group(
×
3770
                        3,
3771
                        effects_meta
×
3772
                            .init_render_indirect_bind_group
×
3773
                            .as_ref()
×
3774
                            .unwrap(),
×
3775
                        &[
×
3776
                            render_effect_indirect_offset as u32,
×
3777
                            first_render_group_indirect_offset as u32,
×
3778
                        ],
3779
                    );
3780
                    compute_pass.dispatch_workgroups(workgroup_count, 1, 1);
×
3781
                    trace!("init compute dispatched");
×
3782
                }
3783
            }
3784
        }
3785

3786
        // Compute indirect dispatch pass
3787
        if effects_meta.spawner_buffer.buffer().is_some()
10✔
3788
            && !effects_meta.spawner_buffer.is_empty()
×
3789
            && effects_meta.dr_indirect_bind_group.is_some()
×
3790
            && effects_meta.sim_params_bind_group.is_some()
×
3791
        {
3792
            // Only if there's an effect
3793
            let mut compute_pass =
×
3794
                render_context
×
3795
                    .command_encoder()
3796
                    .begin_compute_pass(&ComputePassDescriptor {
×
3797
                        label: Some("hanabi:indirect_dispatch"),
×
3798
                        timestamp_writes: None,
×
3799
                    });
3800

3801
            // Dispatch indirect dispatch compute job
3802
            if let Some(indirect_dispatch_pipeline) = &effects_meta.indirect_dispatch_pipeline {
×
3803
                trace!("record commands for indirect dispatch pipeline...");
×
3804

3805
                // FIXME - The `vfx_indirect` shader assumes a contiguous array of ParticleGroup
3806
                // structures. So we need to pass the full array size, and we
3807
                // just update the unused groups for nothing. Otherwise we might
3808
                // update some unused group and miss some used ones, if there's any gap
3809
                // in the array.
3810
                const WORKGROUP_SIZE: u32 = 64;
3811
                let total_group_count = effects_meta.particle_group_buffer.len() as u32;
×
3812
                let workgroup_count = (total_group_count + WORKGROUP_SIZE - 1) / WORKGROUP_SIZE;
×
3813

3814
                // Setup compute pass
3815
                compute_pass.set_pipeline(indirect_dispatch_pipeline);
×
3816
                compute_pass.set_bind_group(
×
3817
                    0,
3818
                    // FIXME - got some unwrap() panic here, investigate... possibly race
3819
                    // condition!
3820
                    effects_meta.dr_indirect_bind_group.as_ref().unwrap(),
×
3821
                    &[],
×
3822
                );
3823
                compute_pass.set_bind_group(
×
3824
                    1,
3825
                    effects_meta.sim_params_bind_group.as_ref().unwrap(),
×
3826
                    &[],
×
3827
                );
3828
                compute_pass.dispatch_workgroups(workgroup_count, 1, 1);
×
3829
                trace!(
×
3830
                    "indirect dispatch compute dispatched: num_batches={} workgroup_count={}",
×
3831
                    total_group_count,
3832
                    workgroup_count
3833
                );
3834
            }
3835
        }
3836

3837
        // Compute update pass
3838
        {
3839
            let mut compute_pass =
10✔
3840
                render_context
10✔
3841
                    .command_encoder()
3842
                    .begin_compute_pass(&ComputePassDescriptor {
10✔
3843
                        label: Some("hanabi:update"),
10✔
3844
                        timestamp_writes: None,
10✔
3845
                    });
3846

3847
            // Dispatch update compute jobs
3848
            for (entity, batches) in self.effect_query.iter_manual(world) {
×
3849
                let effect_cache_id = batches.effect_cache_id;
×
3850

3851
                let Some(particles_update_bind_group) =
×
3852
                    effect_cache.update_bind_group(effect_cache_id)
×
3853
                else {
3854
                    error!(
×
3855
                        "Failed to find update particle buffer bind group for entity {:?}, effect cache ID {:?}",
×
3856
                        entity, effect_cache_id
3857
                    );
3858
                    continue;
×
3859
                };
3860

3861
                let first_update_group_dispatch_buffer_index = batches
3862
                    .dispatch_buffer_indices
3863
                    .first_update_group_dispatch_buffer_index;
3864

3865
                let spawner_base = batches.spawner_base;
3866

3867
                let spawner_buffer_aligned = effects_meta.spawner_buffer.aligned_size();
3868
                assert!(spawner_buffer_aligned >= GpuSpawnerParams::min_size().get() as usize);
3869

3870
                let Some(update_render_indirect_bind_group) = &effect_bind_groups
×
3871
                    .update_render_indirect_bind_groups
×
3872
                    .get(&effect_cache_id)
×
3873
                else {
3874
                    error!(
×
3875
                        "Failed to find update render indirect bind group for effect cache ID: {:?}, IDs present: {:?}",
×
3876
                        effect_cache_id,
×
3877
                        effect_bind_groups
×
3878
                            .update_render_indirect_bind_groups
×
3879
                            .keys()
×
3880
                            .collect::<Vec<_>>()
×
3881
                    );
3882
                    continue;
×
3883
                };
3884

3885
                for (group_index, update_pipeline_id) in
×
3886
                    batches.update_pipeline_ids.iter().enumerate()
3887
                {
3888
                    let Some(update_pipeline) =
×
3889
                        pipeline_cache.get_compute_pipeline(*update_pipeline_id)
×
3890
                    else {
3891
                        if let CachedPipelineState::Err(err) =
×
3892
                            pipeline_cache.get_compute_pipeline_state(*update_pipeline_id)
×
3893
                        {
3894
                            error!(
3895
                                "Failed to find update pipeline #{} for effect {:?}, group {}: {:?}",
×
3896
                                update_pipeline_id.id(),
×
3897
                                entity,
3898
                                group_index,
3899
                                err
3900
                            );
3901
                        }
3902
                        continue;
×
3903
                    };
3904

3905
                    let update_group_dispatch_buffer_offset =
3906
                        effects_meta.gpu_limits.dispatch_indirect_offset(
3907
                            first_update_group_dispatch_buffer_index.0 + group_index as u32,
3908
                        );
3909

3910
                    // for (effect_entity, effect_slice) in effects_meta.entity_map.iter() {
3911
                    // Retrieve the ExtractedEffect from the entity
3912
                    // trace!("effect_entity={:?} effect_slice={:?}", effect_entity,
3913
                    // effect_slice); let effect =
3914
                    // self.effect_query.get_manual(world, *effect_entity).unwrap();
3915

3916
                    // Get the slice to update
3917
                    // let effect_slice = effects_meta.get(&effect_entity);
3918
                    // let effect_group =
3919
                    //     &effects_meta.effect_cache.buffers()[batch.buffer_index as usize];
3920

3921
                    trace!(
3922
                        "record commands for update pipeline of effect {:?} \
×
3923
                        spawner_base={} update_group_dispatch_buffer_offset={}…",
×
3924
                        batches.handle,
3925
                        spawner_base,
3926
                        update_group_dispatch_buffer_offset,
3927
                    );
3928

3929
                    // Setup compute pass
3930
                    // compute_pass.set_pipeline(&effect_group.update_pipeline);
3931
                    compute_pass.set_pipeline(update_pipeline);
×
3932
                    compute_pass.set_bind_group(
×
3933
                        0,
3934
                        effects_meta.sim_params_bind_group.as_ref().unwrap(),
×
3935
                        &[],
×
3936
                    );
3937
                    compute_pass.set_bind_group(1, particles_update_bind_group, &[]);
×
3938
                    compute_pass.set_bind_group(
×
3939
                        2,
3940
                        effects_meta.spawner_bind_group.as_ref().unwrap(),
×
3941
                        &[spawner_base * spawner_buffer_aligned as u32],
×
3942
                    );
3943
                    compute_pass.set_bind_group(3, update_render_indirect_bind_group, &[]);
×
3944

3945
                    if let Some(buffer) = effects_meta.dispatch_indirect_buffer.buffer() {
×
3946
                        trace!(
3947
                            "dispatch_workgroups_indirect: buffer={:?} offset={}",
×
3948
                            buffer,
3949
                            update_group_dispatch_buffer_offset,
3950
                        );
3951
                        compute_pass.dispatch_workgroups_indirect(
×
3952
                            buffer,
×
3953
                            update_group_dispatch_buffer_offset as u64,
×
3954
                        );
3955
                        // TODO - offset
3956
                    }
3957

3958
                    trace!("update compute dispatched");
×
3959
                }
3960
            }
3961
        }
3962

3963
        Ok(())
10✔
3964
    }
3965
}
3966

3967
// FIXME - Remove this, handle it properly with a BufferTable::insert_many() or
3968
// so...
3969
fn allocate_sequential_buffers<T, I>(
×
3970
    buffer_table: &mut BufferTable<T>,
3971
    iterator: I,
3972
) -> BufferTableId
3973
where
3974
    T: Pod + ShaderSize,
3975
    I: Iterator<Item = T>,
3976
{
3977
    let mut first_buffer = None;
×
3978
    for (object_index, object) in iterator.enumerate() {
×
3979
        let buffer = buffer_table.insert(object);
×
3980
        match first_buffer {
×
3981
            None => first_buffer = Some(buffer),
×
3982
            Some(ref first_buffer) => {
×
3983
                if first_buffer.0 + object_index as u32 != buffer.0 {
×
3984
                    error!(
×
3985
                        "Allocator didn't allocate sequential indices (expected {:?}, got {:?}). \
×
3986
                        Expect trouble!",
×
3987
                        first_buffer.0 + object_index as u32,
×
3988
                        buffer.0
×
3989
                    );
3990
                }
3991
            }
3992
        }
3993
    }
3994

3995
    first_buffer.expect("No buffers allocated")
×
3996
}
3997

3998
#[cfg(test)]
3999
mod tests {
4000
    use super::*;
4001

4002
    #[test]
4003
    fn layout_flags() {
4004
        let flags = LayoutFlags::default();
4005
        assert_eq!(flags, LayoutFlags::NONE);
4006
    }
4007

4008
    #[cfg(feature = "gpu_tests")]
4009
    #[test]
4010
    fn gpu_limits() {
4011
        use crate::test_utils::MockRenderer;
4012

4013
        let renderer = MockRenderer::new();
4014
        let device = renderer.device();
4015
        let limits = GpuLimits::from_device(&device);
4016

4017
        // assert!(limits.storage_buffer_align().get() >= 1);
4018
        assert!(
4019
            limits.render_effect_indirect_offset(256)
4020
                >= 256 * GpuRenderEffectMetadata::min_size().get()
4021
        );
4022
        assert!(
4023
            limits.render_group_indirect_offset(256)
4024
                >= 256 * GpuRenderGroupIndirect::min_size().get()
4025
        );
4026
        assert!(
4027
            limits.dispatch_indirect_offset(256) as u64
4028
                >= 256 * GpuDispatchIndirect::min_size().get()
4029
        );
4030
    }
4031
}
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