Skip to main content

bevy_hanabi/render/
mod.rs

1use std::{
2    borrow::Cow,
3    hash::{DefaultHasher, Hash, Hasher},
4    marker::PhantomData,
5    num::{NonZeroU32, NonZeroU64},
6    ops::{Deref, DerefMut, Range},
7    time::Duration,
8    vec,
9};
10
11#[cfg(feature = "2d")]
12use bevy::core_pipeline::core_2d::{Transparent2d, CORE_2D_DEPTH_FORMAT};
13#[cfg(feature = "2d")]
14use bevy::math::FloatOrd;
15#[cfg(feature = "3d")]
16use bevy::{
17    core_pipeline::{
18        core_3d::{
19            AlphaMask3d, Opaque3d, Opaque3dBatchSetKey, Opaque3dBinKey, Transparent3d,
20            CORE_3D_DEPTH_FORMAT,
21        },
22        prepass::{OpaqueNoLightmap3dBatchSetKey, OpaqueNoLightmap3dBinKey},
23    },
24    render::render_phase::{BinnedPhaseItem, ViewBinnedRenderPhases},
25};
26use bevy::{
27    ecs::{
28        change_detection::Tick,
29        prelude::*,
30        system::{lifetimeless::*, SystemParam, SystemState},
31    },
32    log::trace,
33    mesh::MeshVertexBufferLayoutRef,
34    platform::collections::HashMap,
35    prelude::*,
36    render::{
37        mesh::{allocator::MeshAllocator, RenderMesh, RenderMeshBufferInfo},
38        render_asset::RenderAssets,
39        render_graph::{Node, NodeRunError, RenderGraphContext, SlotInfo},
40        render_phase::{
41            Draw, DrawError, DrawFunctions, PhaseItemExtraIndex, SortedPhaseItem,
42            TrackedRenderPass, ViewSortedRenderPhases,
43        },
44        render_resource::*,
45        renderer::{RenderContext, RenderDevice, RenderQueue},
46        sync_world::{MainEntity, RenderEntity, TemporaryRenderEntity},
47        texture::GpuImage,
48        view::{
49            ExtractedView, RenderVisibleEntities, ViewTarget, ViewUniform, ViewUniformOffset,
50            ViewUniforms,
51        },
52        Extract, MainWorld,
53    },
54};
55use bitflags::bitflags;
56use bytemuck::{Pod, Zeroable};
57use effect_cache::{CachedEffect, EffectSlice, SlabState};
58use event::{CachedChildInfo, CachedEffectEvents, CachedParentInfo, GpuChildInfo};
59use fixedbitset::FixedBitSet;
60use gpu_buffer::GpuBuffer;
61use naga_oil::compose::{Composer, NagaModuleDescriptor};
62
63use crate::{
64    asset::{DefaultMesh, EffectAsset},
65    calc_func_id,
66    render::{
67        batch::{BatchInput, EffectDrawBatch, EffectSorter, InitAndUpdatePipelineIds},
68        effect_cache::{
69            AnyDrawIndirectArgs, CachedDrawIndirectArgs, DispatchBufferIndices, SlabId,
70        },
71    },
72    AlphaMode, Attribute, CompiledParticleEffect, EffectProperties, EffectShader, EffectSimulation,
73    EffectSpawner, EffectVisibilityClass, ParticleLayout, PropertyLayout, SimulationCondition,
74    TextureLayout,
75};
76
77mod aligned_buffer_vec;
78mod batch;
79mod buffer_table;
80mod effect_cache;
81mod event;
82mod gpu_buffer;
83mod property;
84mod shader_cache;
85mod sort;
86
87use aligned_buffer_vec::AlignedBufferVec;
88use batch::BatchSpawnInfo;
89pub(crate) use batch::SortedEffectBatches;
90use buffer_table::{BufferTable, BufferTableId};
91pub(crate) use effect_cache::EffectCache;
92pub(crate) use event::{allocate_events, on_remove_cached_effect_events, EventCache};
93pub(crate) use property::{
94    allocate_properties, on_remove_cached_properties, prepare_property_buffers, PropertyBindGroups,
95    PropertyCache,
96};
97use property::{CachedEffectProperties, PropertyBindGroupKey};
98pub use shader_cache::ShaderCache;
99pub(crate) use sort::SortBindGroups;
100
101use self::batch::EffectBatch;
102
103// Size of an indirect index (including both parts of the ping-pong buffer) in
104// bytes.
105const INDIRECT_INDEX_SIZE: u32 = 12;
106
107/// Helper to calculate a hash of a given hashable value.
108fn calc_hash<H: Hash>(value: &H) -> u64 {
109    let mut hasher = DefaultHasher::default();
110    value.hash(&mut hasher);
111    hasher.finish()
112}
113
114/// Source data (buffer and range inside the buffer) to create a buffer binding.
115#[derive(Debug, Clone)]
116pub(crate) struct BufferBindingSource {
117    buffer: Buffer,
118    offset: u32,
119    size: NonZeroU32,
120}
121
122impl BufferBindingSource {
123    /// Get a binding over the source data.
124    pub fn as_binding(&self) -> BindingResource<'_> {
125        BindingResource::Buffer(BufferBinding {
126            buffer: &self.buffer,
127            offset: self.offset as u64 * 4,
128            size: Some(self.size.into()),
129        })
130    }
131}
132
133impl PartialEq for BufferBindingSource {
134    fn eq(&self, other: &Self) -> bool {
135        self.buffer.id() == other.buffer.id()
136            && self.offset == other.offset
137            && self.size == other.size
138    }
139}
140
141impl<'a> From<&'a BufferBindingSource> for BufferBinding<'a> {
142    fn from(value: &'a BufferBindingSource) -> Self {
143        BufferBinding {
144            buffer: &value.buffer,
145            offset: value.offset as u64,
146            size: Some(value.size.into()),
147        }
148    }
149}
150
151/// Simulation parameters, available to all shaders of all effects.
152#[derive(Debug, Default, Clone, Copy, Resource)]
153pub(crate) struct SimParams {
154    /// Current effect system simulation time since startup, in seconds.
155    /// This is based on the [`Time<EffectSimulation>`](EffectSimulation) clock.
156    time: f64,
157    /// Delta time, in seconds, since last effect system update.
158    delta_time: f32,
159
160    /// Current virtual time since startup, in seconds.
161    /// This is based on the [`Time<Virtual>`](Virtual) clock.
162    virtual_time: f64,
163    /// Virtual delta time, in seconds, since last effect system update.
164    virtual_delta_time: f32,
165
166    /// Current real time since startup, in seconds.
167    /// This is based on the [`Time<Real>`](Real) clock.
168    real_time: f64,
169    /// Real delta time, in seconds, since last effect system update.
170    real_delta_time: f32,
171}
172
173/// GPU representation of [`SimParams`], as well as additional per-frame
174/// effect-independent values.
175#[repr(C)]
176#[derive(Debug, Copy, Clone, Pod, Zeroable, ShaderType)]
177struct GpuSimParams {
178    /// Delta time, in seconds, since last effect system update.
179    delta_time: f32,
180    /// Current effect system simulation time since startup, in seconds.
181    ///
182    /// This is a lower-precision variant of [`SimParams::time`].
183    time: f32,
184    /// Virtual delta time, in seconds, since last effect system update.
185    virtual_delta_time: f32,
186    /// Current virtual time since startup, in seconds.
187    ///
188    /// This is a lower-precision variant of [`SimParams::time`].
189    virtual_time: f32,
190    /// Real delta time, in seconds, since last effect system update.
191    real_delta_time: f32,
192    /// Current real time since startup, in seconds.
193    ///
194    /// This is a lower-precision variant of [`SimParams::time`].
195    real_time: f32,
196    /// Total number of effects to update this frame. Used by the indirect
197    /// compute pipeline to cap the compute thread to the actual number of
198    /// effects to process.
199    ///
200    /// This is only used by the `vfx_indirect` compute shader.
201    num_effects: u32,
202}
203
204impl Default for GpuSimParams {
205    fn default() -> Self {
206        Self {
207            delta_time: 0.04,
208            time: 0.0,
209            virtual_delta_time: 0.04,
210            virtual_time: 0.0,
211            real_delta_time: 0.04,
212            real_time: 0.0,
213            num_effects: 0,
214        }
215    }
216}
217
218impl From<SimParams> for GpuSimParams {
219    #[inline]
220    fn from(src: SimParams) -> Self {
221        Self::from(&src)
222    }
223}
224
225impl From<&SimParams> for GpuSimParams {
226    fn from(src: &SimParams) -> Self {
227        Self {
228            delta_time: src.delta_time,
229            time: src.time as f32,
230            virtual_delta_time: src.virtual_delta_time,
231            virtual_time: src.virtual_time as f32,
232            real_delta_time: src.real_delta_time,
233            real_time: src.real_time as f32,
234            ..default()
235        }
236    }
237}
238
239/// Compressed representation of a transform for GPU transfer.
240///
241/// The transform is stored as the three first rows of a transposed [`Mat4`],
242/// assuming the last row is the unit row [`Vec4::W`]. The transposing ensures
243/// that the three values are [`Vec4`] types which are naturally aligned and
244/// without padding when used in WGSL. Without this, storing only the first
245/// three components of each column would introduce padding, and would use the
246/// same storage size on GPU as a full [`Mat4`].
247#[repr(C)]
248#[derive(Debug, Default, Clone, Copy, Pod, Zeroable, ShaderType)]
249pub(crate) struct GpuCompressedTransform {
250    pub x_row: [f32; 4],
251    pub y_row: [f32; 4],
252    pub z_row: [f32; 4],
253}
254
255impl From<Mat4> for GpuCompressedTransform {
256    fn from(value: Mat4) -> Self {
257        let tr = value.transpose();
258        #[cfg(test)]
259        crate::test_utils::assert_approx_eq!(tr.w_axis, Vec4::W);
260        Self {
261            x_row: tr.x_axis.to_array(),
262            y_row: tr.y_axis.to_array(),
263            z_row: tr.z_axis.to_array(),
264        }
265    }
266}
267
268impl From<&Mat4> for GpuCompressedTransform {
269    fn from(value: &Mat4) -> Self {
270        let tr = value.transpose();
271        #[cfg(test)]
272        crate::test_utils::assert_approx_eq!(tr.w_axis, Vec4::W);
273        Self {
274            x_row: tr.x_axis.to_array(),
275            y_row: tr.y_axis.to_array(),
276            z_row: tr.z_axis.to_array(),
277        }
278    }
279}
280
281impl GpuCompressedTransform {
282    /// Returns the translation as represented by this transform.
283    #[allow(dead_code)]
284    pub fn translation(&self) -> Vec3 {
285        Vec3 {
286            x: self.x_row[3],
287            y: self.y_row[3],
288            z: self.z_row[3],
289        }
290    }
291}
292
293/// Extension trait for shader types stored in a WGSL storage buffer.
294pub(crate) trait StorageType {
295    /// Get the aligned size, in bytes, of this type such that it aligns to the
296    /// given alignment, in bytes.
297    ///
298    /// This is mainly used to align GPU types to device requirements.
299    fn aligned_size(alignment: u32) -> NonZeroU64;
300
301    /// Get the WGSL padding code to append to the GPU struct to align it.
302    ///
303    /// This is useful if the struct needs to be bound directly with a dynamic
304    /// bind group offset, which requires the offset to be a multiple of a GPU
305    /// device specific alignment value.
306    fn padding_code(alignment: u32) -> String;
307}
308
309impl<T: ShaderType> StorageType for T {
310    fn aligned_size(alignment: u32) -> NonZeroU64 {
311        NonZeroU64::new(T::min_size().get().next_multiple_of(alignment as u64)).unwrap()
312    }
313
314    fn padding_code(alignment: u32) -> String {
315        let aligned_size = T::aligned_size(alignment);
316        trace!(
317            "Aligning {} to {} bytes as device limits requires. Orignal size: {} bytes. Aligned size: {} bytes.",
318            std::any::type_name::<T>(),
319            alignment,
320            T::min_size().get(),
321            aligned_size
322        );
323
324        // We need to pad the Spawner WGSL struct based on the device padding so that we
325        // can use it as an array element but also has a direct struct binding.
326        if T::min_size() != aligned_size {
327            let padding_size = aligned_size.get() - T::min_size().get();
328            assert!(padding_size % 4 == 0);
329            format!("padding: array<u32, {}>", padding_size / 4)
330        } else {
331            "".to_string()
332        }
333    }
334}
335
336/// GPU representation of spawner parameters.
337#[repr(C)]
338#[derive(Debug, Default, Clone, Copy, Pod, Zeroable, ShaderType)]
339pub(crate) struct GpuSpawnerParams {
340    /// Transform of the effect (origin of the emitter). This is either added to
341    /// emitted particles at spawn time, if the effect simulated in world
342    /// space, or to all simulated particles during rendering if the effect is
343    /// simulated in local space.
344    transform: GpuCompressedTransform,
345    /// Inverse of [`transform`], stored with the same convention.
346    ///
347    /// [`transform`]: Self::transform
348    inverse_transform: GpuCompressedTransform,
349    /// Number of particles to spawn this frame.
350    spawn: i32,
351    /// Spawn seed, for randomized modifiers.
352    seed: u32,
353    /// Index of the pong (read) buffer for indirect indices, used by the render
354    /// shader to fetch particles and render them. Only temporarily stored
355    /// between indirect and render passes, and overwritten each frame by CPU
356    /// upload. This is mostly a hack to transfer a value between those 2
357    /// compute passes.
358    render_pong: u32,
359    /// Index of the [`GpuEffectMetadata`] for this effect.
360    effect_metadata_index: u32,
361    /// Index of the [`GpuDrawIndirect`] or [`GpuDrawIndexedIndirect`] for this
362    /// effect.
363    draw_indirect_index: u32,
364    /// Start offset of the particles and indirect indices into the effect's
365    /// slab, in number of particles (row index).
366    slab_offset: u32,
367    /// Start offset of the particles and indirect indices into the parent
368    /// effect's slab (if the effect has a parent effect), in number of
369    /// particles (row index). This is ignored if the effect has no parent.
370    parent_slab_offset: u32,
371}
372
373/// GPU representation of an indirect compute dispatch input.
374///
375/// Note that unlike most other data structure, this doesn't need to be aligned
376/// (except for the default 4-byte align for most GPU types) to any uniform or
377/// storage buffer offset alignment, because the buffer storing this is only
378/// ever used as input to indirect dispatch commands, and never bound as a
379/// shader resource.
380///
381/// See https://docs.rs/wgpu/latest/wgpu/util/struct.DispatchIndirectArgs.html.
382#[repr(C)]
383#[derive(Debug, Clone, Copy, Pod, Zeroable, ShaderType)]
384pub struct GpuDispatchIndirectArgs {
385    pub x: u32,
386    pub y: u32,
387    pub z: u32,
388}
389
390impl Default for GpuDispatchIndirectArgs {
391    fn default() -> Self {
392        Self { x: 0, y: 1, z: 1 }
393    }
394}
395
396/// GPU representation of an indirect (non-indexed) render input.
397///
398/// Note that unlike most other data structure, this doesn't need to be aligned
399/// (except for the default 4-byte align for most GPU types) to any uniform or
400/// storage buffer offset alignment, because the buffer storing this is only
401/// ever used as input to indirect render commands, and never bound as a shader
402/// resource.
403///
404/// See https://docs.rs/wgpu/latest/wgpu/util/struct.DrawIndirectArgs.html.
405#[repr(C)]
406#[derive(Debug, Clone, Copy, PartialEq, Eq, Pod, Zeroable, ShaderType)]
407pub struct GpuDrawIndirectArgs {
408    pub vertex_count: u32,
409    pub instance_count: u32,
410    pub first_vertex: u32,
411    pub first_instance: u32,
412}
413
414impl Default for GpuDrawIndirectArgs {
415    fn default() -> Self {
416        Self {
417            vertex_count: 0,
418            instance_count: 1,
419            first_vertex: 0,
420            first_instance: 0,
421        }
422    }
423}
424
425/// GPU representation of an indirect indexed render input.
426///
427/// Note that unlike most other data structure, this doesn't need to be aligned
428/// (except for the default 4-byte align for most GPU types) to any uniform or
429/// storage buffer offset alignment, because the buffer storing this is only
430/// ever used as input to indirect render commands, and never bound as a shader
431/// resource.
432///
433/// See https://docs.rs/wgpu/latest/wgpu/util/struct.DrawIndexedIndirectArgs.html.
434#[repr(C)]
435#[derive(Debug, Clone, Copy, PartialEq, Eq, Pod, Zeroable, ShaderType)]
436pub struct GpuDrawIndexedIndirectArgs {
437    pub index_count: u32,
438    pub instance_count: u32,
439    pub first_index: u32,
440    pub base_vertex: i32,
441    pub first_instance: u32,
442}
443
444impl Default for GpuDrawIndexedIndirectArgs {
445    fn default() -> Self {
446        Self {
447            index_count: 0,
448            instance_count: 1,
449            first_index: 0,
450            base_vertex: 0,
451            first_instance: 0,
452        }
453    }
454}
455
456/// Stores metadata about each particle effect.
457///
458/// This is written by the CPU and read by the GPU.
459#[repr(C)]
460#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Pod, Zeroable, ShaderType)]
461pub struct GpuEffectMetadata {
462    //
463    // Some runtime variables modified on GPU only (capacity is constant)
464    /// Effect capacity, in number of particles.
465    pub capacity: u32,
466    // Additional data not part of the required draw indirect args
467    /// Number of alive particles.
468    pub alive_count: u32,
469    /// Cached value of `alive_count` to cap threads in update pass.
470    pub max_update: u32,
471    /// Cached value of `dead_count` to cap threads in init pass.
472    pub max_spawn: u32,
473    /// Index of the ping buffer for particle indices. Init and update compute
474    /// passes always write into the ping buffer and read from the pong buffer.
475    /// The buffers are swapped (ping = 1 - ping) during the indirect dispatch.
476    pub indirect_write_index: u32,
477
478    //
479    // Some real metadata values depending on where the effect instance is allocated.
480    /// Index of the [`GpuDispatchIndirect`] struct inside the global
481    /// [`EffectsMeta::dispatch_indirect_buffer`].
482    pub indirect_dispatch_index: u32,
483    /// Index of the [`GpuDrawIndirect`] or [`GpuDrawIndexedIndirect`] struct
484    /// inside the global [`EffectsMeta::draw_indirect_buffer`] or
485    /// [`EffectsMeta::draw_indexed_indirect_buffer`]. The actual buffer depends
486    /// on whether the mesh is indexed or not, which is stored in
487    /// [`CachedMeshLocation`].
488    pub indirect_draw_index: u32,
489    /// Offset (in u32 count) of the init indirect dispatch struct inside its
490    /// buffer. This avoids having to align those 16-byte structs to the GPU
491    /// alignment (at least 32 bytes, even 256 bytes on some).
492    pub init_indirect_dispatch_index: u32,
493    /// Index of this effect into its parent's ChildInfo array
494    /// ([`EffectChildren::effect_cache_ids`] and its associated GPU
495    /// array). This starts at zero for the first child of each effect, and is
496    /// only unique per parent, not globally. Only available if this effect is a
497    /// child of another effect (i.e. if it has a parent).
498    pub local_child_index: u32,
499    /// For children, global index of the ChildInfo into the shared array.
500    pub global_child_index: u32,
501    /// For parents, base index of the their first ChildInfo into the shared
502    /// array.
503    pub base_child_index: u32,
504
505    /// Particle stride, in number of u32.
506    pub particle_stride: u32,
507    /// Offset from the particle start to the first sort key, in number of u32.
508    pub sort_key_offset: u32,
509    /// Offset from the particle start to the second sort key, in number of u32.
510    pub sort_key2_offset: u32,
511
512    //
513    // Again some runtime-only GPU-mutated data
514    /// Atomic counter incremented each time a particle spawns. Useful for
515    /// things like RIBBON_ID or any other use where a unique value is needed.
516    /// The value loops back after some time, but unless some particle lives
517    /// forever there's little chance of repetition.
518    pub particle_counter: u32,
519}
520
521/// Single init fill dispatch item in an [`InitFillDispatchQueue`].
522#[derive(Debug)]
523pub(super) struct InitFillDispatchItem {
524    /// Index of the source [`GpuChildInfo`] entry to read the event count from.
525    pub global_child_index: u32,
526    /// Index of the [`GpuDispatchIndirect`] entry to write the workgroup count
527    /// to.
528    pub dispatch_indirect_index: u32,
529}
530
531/// Queue of fill dispatch operations for the init indirect pass.
532///
533/// The queue stores the init fill dispatch operations for the current frame,
534/// without the reference to the source and destination buffers, which may be
535/// reallocated later in the frame. This allows enqueuing operations during the
536/// prepare rendering phase, while deferring GPU buffer (re-)allocation to a
537/// later stage.
538#[derive(Debug, Default, Resource)]
539pub(super) struct InitFillDispatchQueue {
540    queue: Vec<InitFillDispatchItem>,
541    submitted_queue_index: Option<u32>,
542}
543
544impl InitFillDispatchQueue {
545    /// Clear the queue.
546    #[inline]
547    pub fn clear(&mut self) {
548        self.queue.clear();
549        self.submitted_queue_index = None;
550    }
551
552    /// Check if the queue is empty.
553    #[inline]
554    pub fn is_empty(&self) -> bool {
555        self.queue.is_empty()
556    }
557
558    /// Enqueue a new operation.
559    #[inline]
560    pub fn enqueue(&mut self, global_child_index: u32, dispatch_indirect_index: u32) {
561        assert!(global_child_index != u32::MAX);
562        self.queue.push(InitFillDispatchItem {
563            global_child_index,
564            dispatch_indirect_index,
565        });
566    }
567
568    /// Submit pending operations for this frame.
569    pub fn submit(
570        &mut self,
571        src_buffer: &Buffer,
572        dst_buffer: &Buffer,
573        gpu_buffer_operations: &mut GpuBufferOperations,
574    ) {
575        if self.queue.is_empty() {
576            return;
577        }
578
579        // Sort by source. We can only batch if the destination is also contiguous, so
580        // we can check with a linear walk if the source is already sorted.
581        self.queue
582            .sort_unstable_by_key(|item| item.global_child_index);
583
584        let mut fill_queue = GpuBufferOperationQueue::new();
585
586        // Batch and schedule all init indirect dispatch operations
587        assert!(
588            self.queue[0].global_child_index != u32::MAX,
589            "Global child index not initialized"
590        );
591        let mut src_start = self.queue[0].global_child_index;
592        let mut dst_start = self.queue[0].dispatch_indirect_index;
593        let mut src_end = src_start + 1;
594        let mut dst_end = dst_start + 1;
595        let src_stride = GpuChildInfo::min_size().get() as u32 / 4;
596        let dst_stride = GpuDispatchIndirectArgs::SHADER_SIZE.get() as u32 / 4;
597        for i in 1..self.queue.len() {
598            let InitFillDispatchItem {
599                global_child_index: src,
600                dispatch_indirect_index: dst,
601            } = self.queue[i];
602            if src != src_end || dst != dst_end {
603                let count = src_end - src_start;
604                debug_assert_eq!(count, dst_end - dst_start);
605                let args = GpuBufferOperationArgs {
606                    src_offset: src_start * src_stride + 1,
607                    src_stride,
608                    dst_offset: dst_start * dst_stride,
609                    dst_stride,
610                    count,
611                };
612                trace!(
613                "enqueue_init_fill(): src:global_child_index={} dst:init_indirect_dispatch_index={} args={:?} src_buffer={:?} dst_buffer={:?}",
614                src_start,
615                dst_start,
616                args,
617                src_buffer.id(),
618                dst_buffer.id(),
619            );
620                fill_queue.enqueue(
621                    GpuBufferOperationType::FillDispatchArgs,
622                    args,
623                    src_buffer.clone(),
624                    0,
625                    None,
626                    dst_buffer.clone(),
627                    0,
628                    None,
629                );
630                src_start = src;
631                dst_start = dst;
632            }
633            src_end = src + 1;
634            dst_end = dst + 1;
635        }
636        if src_start != src_end || dst_start != dst_end {
637            let count = src_end - src_start;
638            debug_assert_eq!(count, dst_end - dst_start);
639            let args = GpuBufferOperationArgs {
640                src_offset: src_start * src_stride + 1,
641                src_stride,
642                dst_offset: dst_start * dst_stride,
643                dst_stride,
644                count,
645            };
646            trace!(
647            "IFDA::submit(): src:global_child_index={} dst:init_indirect_dispatch_index={} args={:?} src_buffer={:?} dst_buffer={:?}",
648            src_start,
649            dst_start,
650            args,
651            src_buffer.id(),
652            dst_buffer.id(),
653        );
654            fill_queue.enqueue(
655                GpuBufferOperationType::FillDispatchArgs,
656                args,
657                src_buffer.clone(),
658                0,
659                None,
660                dst_buffer.clone(),
661                0,
662                None,
663            );
664        }
665
666        debug_assert!(self.submitted_queue_index.is_none());
667        if !fill_queue.operation_queue.is_empty() {
668            self.submitted_queue_index = Some(gpu_buffer_operations.submit(fill_queue));
669        }
670    }
671}
672
673/// Compute pipeline to run the `vfx_indirect` dispatch workgroup calculation
674/// shader.
675#[derive(Resource)]
676pub(crate) struct DispatchIndirectPipeline {
677    /// Layout of bind group sim_params@0.
678    sim_params_bind_group_layout_desc: BindGroupLayoutDescriptor,
679    /// Layout of bind group effect_metadata@1.
680    effect_metadata_bind_group_layout_desc: BindGroupLayoutDescriptor,
681    /// Layout of bind group spawner@2.
682    spawner_bind_group_layout_desc: BindGroupLayoutDescriptor,
683    /// Layout of bind group child_infos@3.
684    child_infos_bind_group_layout_desc: BindGroupLayoutDescriptor,
685    /// Shader when no GPU events are used (no bind group @3).
686    indirect_shader_noevent: Handle<Shader>,
687    /// Shader when GPU events are used (bind group @3 present).
688    indirect_shader_events: Handle<Shader>,
689}
690
691impl FromWorld for DispatchIndirectPipeline {
692    fn from_world(world: &mut World) -> Self {
693        let render_device = world.get_resource::<RenderDevice>().unwrap();
694
695        // Copy the indirect pipeline shaders to self, because we can't access anything
696        // else during pipeline specialization.
697        let (indirect_shader_noevent, indirect_shader_events) = {
698            let effects_meta = world.get_resource::<EffectsMeta>().unwrap();
699            (
700                effects_meta.indirect_shader_noevent.clone(),
701                effects_meta.indirect_shader_events.clone(),
702            )
703        };
704
705        let storage_alignment = render_device.limits().min_storage_buffer_offset_alignment;
706        let effect_metadata_size = GpuEffectMetadata::aligned_size(storage_alignment);
707        let spawner_min_binding_size = GpuSpawnerParams::aligned_size(storage_alignment);
708
709        // @group(0) @binding(0) var<uniform> sim_params : SimParams;
710        trace!("GpuSimParams: min_size={}", GpuSimParams::min_size());
711        let sim_params_bind_group_layout = BindGroupLayoutDescriptor::new(
712            "hanabi:bind_group_layout:dispatch_indirect:sim_params",
713            &[BindGroupLayoutEntry {
714                binding: 0,
715                visibility: ShaderStages::COMPUTE,
716                ty: BindingType::Buffer {
717                    ty: BufferBindingType::Uniform,
718                    has_dynamic_offset: false,
719                    min_binding_size: Some(GpuSimParams::min_size()),
720                },
721                count: None,
722            }],
723        );
724
725        trace!(
726            "GpuEffectMetadata: min_size={} padded_size={}",
727            GpuEffectMetadata::min_size(),
728            effect_metadata_size,
729        );
730        let effect_metadata_bind_group_layout = BindGroupLayoutDescriptor::new(
731            "hanabi:bind_group_layout:dispatch_indirect:effect_metadata@1",
732            &[
733                // @group(0) @binding(0) var<storage, read_write> effect_metadata_buffer :
734                // array<u32>;
735                BindGroupLayoutEntry {
736                    binding: 0,
737                    visibility: ShaderStages::COMPUTE,
738                    ty: BindingType::Buffer {
739                        ty: BufferBindingType::Storage { read_only: false },
740                        has_dynamic_offset: false,
741                        min_binding_size: Some(effect_metadata_size),
742                    },
743                    count: None,
744                },
745                // @group(0) @binding(1) var<storage, read_write> dispatch_indirect_buffer :
746                // array<u32>;
747                BindGroupLayoutEntry {
748                    binding: 1,
749                    visibility: ShaderStages::COMPUTE,
750                    ty: BindingType::Buffer {
751                        ty: BufferBindingType::Storage { read_only: false },
752                        has_dynamic_offset: false,
753                        min_binding_size: Some(
754                            NonZeroU64::new(INDIRECT_INDEX_SIZE as u64).unwrap(),
755                        ),
756                    },
757                    count: None,
758                },
759                // @group(0) @binding(2) var<storage, read_write> draw_indirect_buffer :
760                // array<u32>;
761                BindGroupLayoutEntry {
762                    binding: 2,
763                    visibility: ShaderStages::COMPUTE,
764                    ty: BindingType::Buffer {
765                        ty: BufferBindingType::Storage { read_only: false },
766                        has_dynamic_offset: false,
767                        min_binding_size: Some(GpuDrawIndexedIndirectArgs::SHADER_SIZE),
768                    },
769                    count: None,
770                },
771            ],
772        );
773
774        // @group(2) @binding(0) var<storage, read_write> spawner_buffer :
775        // array<Spawner>;
776        let spawner_bind_group_layout = BindGroupLayoutDescriptor::new(
777            "hanabi:bind_group_layout:dispatch_indirect:spawner@2",
778            &[BindGroupLayoutEntry {
779                binding: 0,
780                visibility: ShaderStages::COMPUTE,
781                ty: BindingType::Buffer {
782                    ty: BufferBindingType::Storage { read_only: false },
783                    has_dynamic_offset: false,
784                    min_binding_size: Some(spawner_min_binding_size),
785                },
786                count: None,
787            }],
788        );
789
790        // @group(3) @binding(0) var<storage, read_write> child_info_buffer :
791        // ChildInfoBuffer;
792        let child_infos_bind_group_layout = BindGroupLayoutDescriptor::new(
793            "hanabi:bind_group_layout:dispatch_indirect:child_infos",
794            &[BindGroupLayoutEntry {
795                binding: 0,
796                visibility: ShaderStages::COMPUTE,
797                ty: BindingType::Buffer {
798                    ty: BufferBindingType::Storage { read_only: false },
799                    has_dynamic_offset: false,
800                    min_binding_size: Some(GpuChildInfo::min_size()),
801                },
802                count: None,
803            }],
804        );
805
806        Self {
807            sim_params_bind_group_layout_desc: sim_params_bind_group_layout,
808            effect_metadata_bind_group_layout_desc: effect_metadata_bind_group_layout,
809            spawner_bind_group_layout_desc: spawner_bind_group_layout,
810            child_infos_bind_group_layout_desc: child_infos_bind_group_layout,
811            indirect_shader_noevent,
812            indirect_shader_events,
813        }
814    }
815}
816
817#[derive(Debug, Clone, PartialEq, Eq, Hash)]
818pub(crate) struct DispatchIndirectPipelineKey {
819    /// True if any allocated effect uses GPU spawn events. In that case, the
820    /// pipeline is specialized to clear all GPU events each frame after the
821    /// indirect init pass consumed them to spawn particles, and before the
822    /// update pass optionally produce more events.
823    /// Key: HAS_GPU_SPAWN_EVENTS
824    has_events: bool,
825}
826
827impl SpecializedComputePipeline for DispatchIndirectPipeline {
828    type Key = DispatchIndirectPipelineKey;
829
830    fn specialize(&self, key: Self::Key) -> ComputePipelineDescriptor {
831        trace!(
832            "Specializing indirect pipeline (has_events={})",
833            key.has_events
834        );
835
836        let mut shader_defs = Vec::with_capacity(2);
837        // Spawner struct needs to be defined with padding, because it's bound as an
838        // array
839        shader_defs.push("SPAWNER_PADDING".into());
840        if key.has_events {
841            shader_defs.push("HAS_GPU_SPAWN_EVENTS".into());
842        }
843
844        let mut layout = Vec::with_capacity(4);
845        layout.push(self.sim_params_bind_group_layout_desc.clone());
846        layout.push(self.effect_metadata_bind_group_layout_desc.clone());
847        layout.push(self.spawner_bind_group_layout_desc.clone());
848        if key.has_events {
849            layout.push(self.child_infos_bind_group_layout_desc.clone());
850        }
851
852        let label = format!(
853            "hanabi:compute_pipeline:dispatch_indirect{}",
854            if key.has_events {
855                "_events"
856            } else {
857                "_noevent"
858            }
859        );
860
861        ComputePipelineDescriptor {
862            label: Some(label.into()),
863            layout,
864            shader: if key.has_events {
865                self.indirect_shader_events.clone()
866            } else {
867                self.indirect_shader_noevent.clone()
868            },
869            shader_defs,
870            entry_point: Some("main".into()),
871            push_constant_ranges: vec![],
872            zero_initialize_workgroup_memory: false,
873        }
874    }
875}
876
877/// Type of GPU buffer operation.
878#[derive(Debug, Clone, Copy, PartialEq, Eq)]
879pub(super) enum GpuBufferOperationType {
880    /// Clear the destination buffer to zero.
881    ///
882    /// The source parameters [`src_offset`] and [`src_stride`] are ignored.
883    ///
884    /// [`src_offset`]: crate::GpuBufferOperationArgs::src_offset
885    /// [`src_stride`]: crate::GpuBufferOperationArgs::src_stride
886    #[allow(dead_code)]
887    Zero,
888    /// Copy a source buffer into a destination buffer.
889    ///
890    /// The source can have a stride between each `u32` copied. The destination
891    /// is always a contiguous buffer.
892    #[allow(dead_code)]
893    Copy,
894    /// Fill the arguments for a later indirect dispatch call.
895    ///
896    /// This is similar to a copy, but will round up the source value to the
897    /// number of threads per workgroup (64) before writing it into the
898    /// destination.
899    FillDispatchArgs,
900    /// Fill the arguments for a later indirect dispatch call.
901    ///
902    /// This is the same as [`FillDispatchArgs`], but the source element count
903    /// is read from the fourth entry in the destination buffer directly,
904    /// and the source buffer and source arguments are unused.
905    #[allow(dead_code)]
906    FillDispatchArgsSelf,
907}
908
909/// GPU representation of the arguments of a block operation on a buffer.
910#[repr(C)]
911#[derive(Debug, Copy, Clone, PartialEq, Eq, Pod, Zeroable, ShaderType)]
912pub(super) struct GpuBufferOperationArgs {
913    /// Offset, as u32 count, where the operation starts in the source buffer.
914    src_offset: u32,
915    /// Stride, as u32 count, between elements in the source buffer.
916    src_stride: u32,
917    /// Offset, as u32 count, where the operation starts in the destination
918    /// buffer.
919    dst_offset: u32,
920    /// Stride, as u32 count, between elements in the destination buffer.
921    dst_stride: u32,
922    /// Number of u32 elements to process for this operation.
923    count: u32,
924}
925
926#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
927struct QueuedOperationBindGroupKey {
928    src_buffer: BufferId,
929    src_binding_size: Option<NonZeroU32>,
930    dst_buffer: BufferId,
931    dst_binding_size: Option<NonZeroU32>,
932}
933
934#[derive(Debug, Clone)]
935struct QueuedOperation {
936    op: GpuBufferOperationType,
937    args_index: u32,
938    src_buffer: Buffer,
939    src_binding_offset: u32,
940    src_binding_size: Option<NonZeroU32>,
941    dst_buffer: Buffer,
942    dst_binding_offset: u32,
943    dst_binding_size: Option<NonZeroU32>,
944}
945
946impl From<&QueuedOperation> for QueuedOperationBindGroupKey {
947    fn from(value: &QueuedOperation) -> Self {
948        Self {
949            src_buffer: value.src_buffer.id(),
950            src_binding_size: value.src_binding_size,
951            dst_buffer: value.dst_buffer.id(),
952            dst_binding_size: value.dst_binding_size,
953        }
954    }
955}
956
957/// Queue of GPU buffer operations.
958///
959/// The queue records a series of ordered operations on GPU buffers. It can be
960/// submitted for this frame via [`GpuBufferOperations::submit()`], and
961/// subsequently dispatched as a compute pass via
962/// [`GpuBufferOperations::dispatch()`].
963pub struct GpuBufferOperationQueue {
964    /// Operation arguments.
965    args: Vec<GpuBufferOperationArgs>,
966    /// Queued operations.
967    operation_queue: Vec<QueuedOperation>,
968}
969
970impl GpuBufferOperationQueue {
971    /// Create a new empty queue.
972    pub fn new() -> Self {
973        Self {
974            args: vec![],
975            operation_queue: vec![],
976        }
977    }
978
979    /// Enqueue a generic operation.
980    pub fn enqueue(
981        &mut self,
982        op: GpuBufferOperationType,
983        args: GpuBufferOperationArgs,
984        src_buffer: Buffer,
985        src_binding_offset: u32,
986        src_binding_size: Option<NonZeroU32>,
987        dst_buffer: Buffer,
988        dst_binding_offset: u32,
989        dst_binding_size: Option<NonZeroU32>,
990    ) -> u32 {
991        trace!(
992            "Queue {:?} op: args={:?} src_buffer={:?} src_binding_offset={} src_binding_size={:?} dst_buffer={:?} dst_binding_offset={} dst_binding_size={:?}",
993            op,
994            args,
995            src_buffer,
996            src_binding_offset,
997            src_binding_size,
998            dst_buffer,
999            dst_binding_offset,
1000            dst_binding_size,
1001        );
1002        let args_index = self.args.len() as u32;
1003        self.args.push(args);
1004        self.operation_queue.push(QueuedOperation {
1005            op,
1006            args_index,
1007            src_buffer,
1008            src_binding_offset,
1009            src_binding_size,
1010            dst_buffer,
1011            dst_binding_offset,
1012            dst_binding_size,
1013        });
1014        args_index
1015    }
1016}
1017
1018/// GPU buffer operations for this frame.
1019///
1020/// This resource contains a list of submitted [`GpuBufferOperationQueue`] for
1021/// the current frame, and ensures the bind groups for those operations are up
1022/// to date.
1023#[derive(Resource)]
1024pub(super) struct GpuBufferOperations {
1025    /// Arguments for the buffer operations submitted this frame.
1026    args_buffer: AlignedBufferVec<GpuBufferOperationArgs>,
1027
1028    /// Bind groups for the submitted operations.
1029    bind_groups: HashMap<QueuedOperationBindGroupKey, BindGroup>,
1030
1031    /// Submitted queues for this frame.
1032    queues: Vec<Vec<QueuedOperation>>,
1033}
1034
1035impl FromWorld for GpuBufferOperations {
1036    fn from_world(world: &mut World) -> Self {
1037        let render_device = world.get_resource::<RenderDevice>().unwrap();
1038        let align = render_device.limits().min_uniform_buffer_offset_alignment;
1039        Self::new(align)
1040    }
1041}
1042
1043impl GpuBufferOperations {
1044    pub fn new(align: u32) -> Self {
1045        let args_buffer = AlignedBufferVec::new(
1046            BufferUsages::UNIFORM,
1047            Some(NonZeroU64::new(align as u64).unwrap()),
1048            Some("hanabi:buffer:gpu_operation_args".to_string()),
1049        );
1050        Self {
1051            args_buffer,
1052            bind_groups: default(),
1053            queues: vec![],
1054        }
1055    }
1056
1057    /// Clear the queue and begin recording operations for a new frame.
1058    pub fn begin_frame(&mut self) {
1059        self.args_buffer.clear();
1060        self.bind_groups.clear(); // for now; might consider caching frame-to-frame
1061        self.queues.clear();
1062    }
1063
1064    /// Submit a recorded queue.
1065    ///
1066    /// # Panics
1067    ///
1068    /// Panics if the queue submitted is empty.
1069    pub fn submit(&mut self, mut queue: GpuBufferOperationQueue) -> u32 {
1070        assert!(!queue.operation_queue.is_empty());
1071        let queue_index = self.queues.len() as u32;
1072        for qop in &mut queue.operation_queue {
1073            qop.args_index = self.args_buffer.push(queue.args[qop.args_index as usize]) as u32;
1074        }
1075        self.queues.push(queue.operation_queue);
1076        queue_index
1077    }
1078
1079    /// Finish recording operations for this frame, and schedule buffer writes
1080    /// to GPU.
1081    pub fn end_frame(&mut self, device: &RenderDevice, render_queue: &RenderQueue) {
1082        assert_eq!(
1083            self.args_buffer.len(),
1084            self.queues.iter().fold(0, |len, q| len + q.len())
1085        );
1086
1087        // Upload to GPU buffer
1088        self.args_buffer.write_buffer(device, render_queue);
1089    }
1090
1091    /// Create all necessary bind groups for all queued operations.
1092    pub fn create_bind_groups(
1093        &mut self,
1094        render_device: &RenderDevice,
1095        utils_pipeline: &UtilsPipeline,
1096    ) {
1097        trace!(
1098            "Creating bind groups for {} operation queues...",
1099            self.queues.len()
1100        );
1101        for queue in &self.queues {
1102            for qop in queue {
1103                let key: QueuedOperationBindGroupKey = qop.into();
1104                self.bind_groups.entry(key).or_insert_with(|| {
1105                    let src_id: NonZeroU32 = qop.src_buffer.id().into();
1106                    let dst_id: NonZeroU32 = qop.dst_buffer.id().into();
1107                    let label = format!("hanabi:bind_group:util_{}_{}", src_id.get(), dst_id.get());
1108                    let use_dynamic_offset = matches!(qop.op, GpuBufferOperationType::FillDispatchArgs);
1109                    let bind_group_layout =
1110                        utils_pipeline.bind_group_layout(qop.op, use_dynamic_offset);
1111                    let (src_offset, dst_offset) = if use_dynamic_offset {
1112                        (0, 0)
1113                    } else {
1114                        (qop.src_binding_offset as u64, qop.dst_binding_offset as u64)
1115                    };
1116                    trace!(
1117                        "-> Creating new bind group '{}': src#{} (@+{}B:{:?}B) dst#{} (@+{}B:{:?}B)",
1118                        label,
1119                        src_id,
1120                        src_offset,
1121                        qop.src_binding_size,
1122                        dst_id,
1123                        dst_offset,
1124                        qop.dst_binding_size,
1125                    );
1126                    render_device.create_bind_group(
1127                        Some(&label[..]),
1128                        bind_group_layout,
1129                        &[
1130                            BindGroupEntry {
1131                                binding: 0,
1132                                resource: BindingResource::Buffer(BufferBinding {
1133                                    buffer: self.args_buffer.buffer().unwrap(),
1134                                    offset: 0,
1135                                    // We always bind exactly 1 row of arguments
1136                                    size: Some(
1137                                        NonZeroU64::new(self.args_buffer.aligned_size() as u64)
1138                                            .unwrap(),
1139                                    ),
1140                                }),
1141                            },
1142                            BindGroupEntry {
1143                                binding: 1,
1144                                resource: BindingResource::Buffer(BufferBinding {
1145                                    buffer: &qop.src_buffer,
1146                                    offset: src_offset,
1147                                    size: qop.src_binding_size.map(Into::into),
1148                                }),
1149                            },
1150                            BindGroupEntry {
1151                                binding: 2,
1152                                resource: BindingResource::Buffer(BufferBinding {
1153                                    buffer: &qop.dst_buffer,
1154                                    offset: dst_offset,
1155                                    size: qop.dst_binding_size.map(Into::into),
1156                                }),
1157                            },
1158                        ],
1159                    )
1160                });
1161            }
1162        }
1163    }
1164
1165    /// Dispatch a submitted queue by index.
1166    ///
1167    /// This creates a new, optionally labelled, compute pass, and records to
1168    /// the render context a series of compute workgroup dispatch, one for each
1169    /// enqueued operation.
1170    ///
1171    /// The compute pipeline(s) used for each operation are fetched from the
1172    /// [`UtilsPipeline`], and the associated bind groups are used from a
1173    /// previous call to [`Self::create_bind_groups()`].
1174    pub fn dispatch(
1175        &self,
1176        index: u32,
1177        render_context: &mut RenderContext,
1178        utils_pipeline: &UtilsPipeline,
1179        compute_pass_label: Option<&str>,
1180    ) {
1181        let queue = &self.queues[index as usize];
1182        trace!(
1183            "Recording GPU commands for queue #{} ({} ops)...",
1184            index,
1185            queue.len(),
1186        );
1187
1188        if queue.is_empty() {
1189            return;
1190        }
1191
1192        let mut compute_pass =
1193            render_context
1194                .command_encoder()
1195                .begin_compute_pass(&ComputePassDescriptor {
1196                    label: compute_pass_label,
1197                    timestamp_writes: None,
1198                });
1199
1200        let mut prev_op = None;
1201        for qop in queue {
1202            trace!("qop={:?}", qop);
1203
1204            if Some(qop.op) != prev_op {
1205                compute_pass.set_pipeline(utils_pipeline.get_pipeline(qop.op));
1206                prev_op = Some(qop.op);
1207            }
1208
1209            let key: QueuedOperationBindGroupKey = qop.into();
1210            if let Some(bind_group) = self.bind_groups.get(&key) {
1211                let args_offset = self.args_buffer.dynamic_offset(qop.args_index as usize);
1212                let use_dynamic_offset = matches!(qop.op, GpuBufferOperationType::FillDispatchArgs);
1213                let (src_offset, dst_offset) = if use_dynamic_offset {
1214                    (qop.src_binding_offset, qop.dst_binding_offset)
1215                } else {
1216                    (0, 0)
1217                };
1218                compute_pass.set_bind_group(0, bind_group, &[args_offset, src_offset, dst_offset]);
1219                trace!(
1220                    "set bind group with args_offset=+{}B src_offset=+{}B dst_offset=+{}B",
1221                    args_offset,
1222                    src_offset,
1223                    dst_offset
1224                );
1225            } else {
1226                error!("GPU fill dispatch buffer operation bind group not found for buffers src#{:?} dst#{:?}", qop.src_buffer.id(), qop.dst_buffer.id());
1227                continue;
1228            }
1229
1230            // Dispatch the operations for this buffer
1231            const WORKGROUP_SIZE: u32 = 64;
1232            let num_ops = 1u32; // TODO - batching!
1233            let workgroup_count = num_ops.div_ceil(WORKGROUP_SIZE);
1234            compute_pass.dispatch_workgroups(workgroup_count, 1, 1);
1235            trace!(
1236                "-> fill dispatch compute dispatched: num_ops={} workgroup_count={}",
1237                num_ops,
1238                workgroup_count
1239            );
1240        }
1241    }
1242}
1243
1244/// Compute pipeline to run the `vfx_utils` shader.
1245#[derive(Resource)]
1246pub(crate) struct UtilsPipeline {
1247    #[allow(dead_code)]
1248    bind_group_layout: BindGroupLayout,
1249    bind_group_layout_dyn: BindGroupLayout,
1250    bind_group_layout_no_src: BindGroupLayout,
1251    pipelines: [ComputePipeline; 4],
1252}
1253
1254impl FromWorld for UtilsPipeline {
1255    fn from_world(world: &mut World) -> Self {
1256        let render_device = world.get_resource::<RenderDevice>().unwrap();
1257
1258        let bind_group_layout = render_device.create_bind_group_layout(
1259            "hanabi:bind_group_layout:utils",
1260            &[
1261                BindGroupLayoutEntry {
1262                    binding: 0,
1263                    visibility: ShaderStages::COMPUTE,
1264                    ty: BindingType::Buffer {
1265                        ty: BufferBindingType::Uniform,
1266                        has_dynamic_offset: false,
1267                        min_binding_size: Some(GpuBufferOperationArgs::min_size()),
1268                    },
1269                    count: None,
1270                },
1271                BindGroupLayoutEntry {
1272                    binding: 1,
1273                    visibility: ShaderStages::COMPUTE,
1274                    ty: BindingType::Buffer {
1275                        ty: BufferBindingType::Storage { read_only: true },
1276                        has_dynamic_offset: false,
1277                        min_binding_size: NonZeroU64::new(4),
1278                    },
1279                    count: None,
1280                },
1281                BindGroupLayoutEntry {
1282                    binding: 2,
1283                    visibility: ShaderStages::COMPUTE,
1284                    ty: BindingType::Buffer {
1285                        ty: BufferBindingType::Storage { read_only: false },
1286                        has_dynamic_offset: false,
1287                        min_binding_size: NonZeroU64::new(4),
1288                    },
1289                    count: None,
1290                },
1291            ],
1292        );
1293
1294        let pipeline_layout = render_device.create_pipeline_layout(&PipelineLayoutDescriptor {
1295            label: Some("hanabi:pipeline_layout:utils"),
1296            bind_group_layouts: &[&bind_group_layout],
1297            push_constant_ranges: &[],
1298        });
1299
1300        let bind_group_layout_dyn = render_device.create_bind_group_layout(
1301            "hanabi:bind_group_layout:utils_dyn",
1302            &[
1303                BindGroupLayoutEntry {
1304                    binding: 0,
1305                    visibility: ShaderStages::COMPUTE,
1306                    ty: BindingType::Buffer {
1307                        ty: BufferBindingType::Uniform,
1308                        has_dynamic_offset: true,
1309                        min_binding_size: Some(GpuBufferOperationArgs::min_size()),
1310                    },
1311                    count: None,
1312                },
1313                BindGroupLayoutEntry {
1314                    binding: 1,
1315                    visibility: ShaderStages::COMPUTE,
1316                    ty: BindingType::Buffer {
1317                        ty: BufferBindingType::Storage { read_only: true },
1318                        has_dynamic_offset: true,
1319                        min_binding_size: NonZeroU64::new(4),
1320                    },
1321                    count: None,
1322                },
1323                BindGroupLayoutEntry {
1324                    binding: 2,
1325                    visibility: ShaderStages::COMPUTE,
1326                    ty: BindingType::Buffer {
1327                        ty: BufferBindingType::Storage { read_only: false },
1328                        has_dynamic_offset: true,
1329                        min_binding_size: NonZeroU64::new(4),
1330                    },
1331                    count: None,
1332                },
1333            ],
1334        );
1335
1336        let pipeline_layout_dyn = render_device.create_pipeline_layout(&PipelineLayoutDescriptor {
1337            label: Some("hanabi:pipeline_layout:utils_dyn"),
1338            bind_group_layouts: &[&bind_group_layout_dyn],
1339            push_constant_ranges: &[],
1340        });
1341
1342        let bind_group_layout_no_src = render_device.create_bind_group_layout(
1343            "hanabi:bind_group_layout:utils_no_src",
1344            &[
1345                BindGroupLayoutEntry {
1346                    binding: 0,
1347                    visibility: ShaderStages::COMPUTE,
1348                    ty: BindingType::Buffer {
1349                        ty: BufferBindingType::Uniform,
1350                        has_dynamic_offset: false,
1351                        min_binding_size: Some(GpuBufferOperationArgs::min_size()),
1352                    },
1353                    count: None,
1354                },
1355                BindGroupLayoutEntry {
1356                    binding: 2,
1357                    visibility: ShaderStages::COMPUTE,
1358                    ty: BindingType::Buffer {
1359                        ty: BufferBindingType::Storage { read_only: false },
1360                        has_dynamic_offset: false,
1361                        min_binding_size: NonZeroU64::new(4),
1362                    },
1363                    count: None,
1364                },
1365            ],
1366        );
1367
1368        let pipeline_layout_no_src =
1369            render_device.create_pipeline_layout(&PipelineLayoutDescriptor {
1370                label: Some("hanabi:pipeline_layout:utils_no_src"),
1371                bind_group_layouts: &[&bind_group_layout_no_src],
1372                push_constant_ranges: &[],
1373            });
1374
1375        let shader_code = include_str!("vfx_utils.wgsl");
1376
1377        // Resolve imports. Because we don't insert this shader into Bevy' pipeline
1378        // cache, we don't get that part "for free", so we have to do it manually here.
1379        let shader_source = {
1380            let mut composer = Composer::default();
1381
1382            let shader_defs = default();
1383
1384            match composer.make_naga_module(NagaModuleDescriptor {
1385                source: shader_code,
1386                file_path: "vfx_utils.wgsl",
1387                shader_defs,
1388                ..Default::default()
1389            }) {
1390                Ok(naga_module) => ShaderSource::Naga(Cow::Owned(naga_module)),
1391                Err(compose_error) => panic!(
1392                    "Failed to compose vfx_utils.wgsl, naga_oil returned: {}",
1393                    compose_error.emit_to_string(&composer)
1394                ),
1395            }
1396        };
1397
1398        debug!("Create utils shader module:\n{}", shader_code);
1399        #[allow(unsafe_code)]
1400        let shader_module = unsafe {
1401            render_device.create_shader_module(ShaderModuleDescriptor {
1402                label: Some("hanabi:shader:utils"),
1403                source: shader_source,
1404            })
1405        };
1406
1407        trace!("Create vfx_utils pipelines...");
1408        let zero_pipeline = render_device.create_compute_pipeline(&RawComputePipelineDescriptor {
1409            label: Some("hanabi:compute_pipeline:zero_buffer"),
1410            layout: Some(&pipeline_layout),
1411            module: &shader_module,
1412            entry_point: Some("zero_buffer"),
1413            compilation_options: PipelineCompilationOptions {
1414                constants: &[],
1415                zero_initialize_workgroup_memory: false,
1416            },
1417            cache: None,
1418        });
1419        let copy_pipeline = render_device.create_compute_pipeline(&RawComputePipelineDescriptor {
1420            label: Some("hanabi:compute_pipeline:copy_buffer"),
1421            layout: Some(&pipeline_layout_dyn),
1422            module: &shader_module,
1423            entry_point: Some("copy_buffer"),
1424            compilation_options: PipelineCompilationOptions {
1425                constants: &[],
1426                zero_initialize_workgroup_memory: false,
1427            },
1428            cache: None,
1429        });
1430        let fill_dispatch_args_pipeline =
1431            render_device.create_compute_pipeline(&RawComputePipelineDescriptor {
1432                label: Some("hanabi:compute_pipeline:fill_dispatch_args"),
1433                layout: Some(&pipeline_layout_dyn),
1434                module: &shader_module,
1435                entry_point: Some("fill_dispatch_args"),
1436                compilation_options: PipelineCompilationOptions {
1437                    constants: &[],
1438                    zero_initialize_workgroup_memory: false,
1439                },
1440                cache: None,
1441            });
1442        let fill_dispatch_args_self_pipeline =
1443            render_device.create_compute_pipeline(&RawComputePipelineDescriptor {
1444                label: Some("hanabi:compute_pipeline:fill_dispatch_args_self"),
1445                layout: Some(&pipeline_layout_no_src),
1446                module: &shader_module,
1447                entry_point: Some("fill_dispatch_args_self"),
1448                compilation_options: PipelineCompilationOptions {
1449                    constants: &[],
1450                    zero_initialize_workgroup_memory: false,
1451                },
1452                cache: None,
1453            });
1454
1455        Self {
1456            bind_group_layout,
1457            bind_group_layout_dyn,
1458            bind_group_layout_no_src,
1459            pipelines: [
1460                zero_pipeline,
1461                copy_pipeline,
1462                fill_dispatch_args_pipeline,
1463                fill_dispatch_args_self_pipeline,
1464            ],
1465        }
1466    }
1467}
1468
1469impl UtilsPipeline {
1470    fn get_pipeline(&self, op: GpuBufferOperationType) -> &ComputePipeline {
1471        match op {
1472            GpuBufferOperationType::Zero => &self.pipelines[0],
1473            GpuBufferOperationType::Copy => &self.pipelines[1],
1474            GpuBufferOperationType::FillDispatchArgs => &self.pipelines[2],
1475            GpuBufferOperationType::FillDispatchArgsSelf => &self.pipelines[3],
1476        }
1477    }
1478
1479    fn bind_group_layout(
1480        &self,
1481        op: GpuBufferOperationType,
1482        with_dynamic_offsets: bool,
1483    ) -> &BindGroupLayout {
1484        if op == GpuBufferOperationType::FillDispatchArgsSelf {
1485            assert!(
1486                !with_dynamic_offsets,
1487                "FillDispatchArgsSelf op cannot use dynamic offset (not implemented)"
1488            );
1489            &self.bind_group_layout_no_src
1490        } else if with_dynamic_offsets {
1491            &self.bind_group_layout_dyn
1492        } else {
1493            &self.bind_group_layout
1494        }
1495    }
1496}
1497
1498#[derive(Resource)]
1499pub(crate) struct ParticlesInitPipeline {
1500    sim_params_layout_desc: BindGroupLayoutDescriptor,
1501}
1502
1503impl Default for ParticlesInitPipeline {
1504    fn default() -> Self {
1505        let sim_params_layout_desc = BindGroupLayoutDescriptor::new(
1506            "hanabi:bind_group_layout:vfx_init:sim_params@0",
1507            // @group(0) @binding(0) var<uniform> sim_params: SimParams;
1508            &[BindGroupLayoutEntry {
1509                binding: 0,
1510                visibility: ShaderStages::COMPUTE,
1511                ty: BindingType::Buffer {
1512                    ty: BufferBindingType::Uniform,
1513                    has_dynamic_offset: false,
1514                    min_binding_size: Some(GpuSimParams::min_size()),
1515                },
1516                count: None,
1517            }],
1518        );
1519
1520        Self {
1521            sim_params_layout_desc,
1522        }
1523    }
1524}
1525
1526bitflags! {
1527    #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1528    pub struct ParticleInitPipelineKeyFlags: u8 {
1529        //const CLONE = (1u8 << 0); // DEPRECATED
1530        const ATTRIBUTE_PREV = (1u8 << 1);
1531        const ATTRIBUTE_NEXT = (1u8 << 2);
1532        const CONSUME_GPU_SPAWN_EVENTS = (1u8 << 3);
1533    }
1534}
1535
1536#[derive(Debug, Clone, PartialEq, Eq, Hash)]
1537pub(crate) struct ParticleInitPipelineKey {
1538    /// Compute shader, with snippets applied, but not preprocessed yet.
1539    shader: Handle<Shader>,
1540    /// Minimum binding size in bytes for the particle layout buffer.
1541    particle_layout_min_binding_size: NonZeroU32,
1542    /// Minimum binding size in bytes for the particle layout buffer of the
1543    /// parent effect, if any.
1544    /// Key: READ_PARENT_PARTICLE
1545    parent_particle_layout_min_binding_size: Option<NonZeroU32>,
1546    /// Pipeline flags.
1547    flags: ParticleInitPipelineKeyFlags,
1548    /// Layout of the particle@1 bind group this pipeline was specialized with.
1549    particle_bind_group_layout_desc: BindGroupLayoutDescriptor,
1550    /// Layout of the spawner@2 bind group this pipeline was specialized with.
1551    spawner_bind_group_layout_desc: BindGroupLayoutDescriptor,
1552    /// Layout of the metadata@3 bind group this pipeline was specialized with.
1553    metadata_bind_group_layout_desc: BindGroupLayoutDescriptor,
1554}
1555
1556impl SpecializedComputePipeline for ParticlesInitPipeline {
1557    type Key = ParticleInitPipelineKey;
1558
1559    fn specialize(&self, key: Self::Key) -> ComputePipelineDescriptor {
1560        // We use the hash to correlate the key content with the GPU resource name
1561        let hash = calc_hash(&key);
1562        trace!("Specializing init pipeline {hash:016X} with key {key:?}");
1563
1564        let mut shader_defs = Vec::with_capacity(4);
1565        if key
1566            .flags
1567            .contains(ParticleInitPipelineKeyFlags::ATTRIBUTE_PREV)
1568        {
1569            shader_defs.push("ATTRIBUTE_PREV".into());
1570        }
1571        if key
1572            .flags
1573            .contains(ParticleInitPipelineKeyFlags::ATTRIBUTE_NEXT)
1574        {
1575            shader_defs.push("ATTRIBUTE_NEXT".into());
1576        }
1577        let consume_gpu_spawn_events = key
1578            .flags
1579            .contains(ParticleInitPipelineKeyFlags::CONSUME_GPU_SPAWN_EVENTS);
1580        if consume_gpu_spawn_events {
1581            shader_defs.push("CONSUME_GPU_SPAWN_EVENTS".into());
1582        }
1583        // FIXME - for now this needs to keep in sync with consume_gpu_spawn_events
1584        if key.parent_particle_layout_min_binding_size.is_some() {
1585            assert!(consume_gpu_spawn_events);
1586            shader_defs.push("READ_PARENT_PARTICLE".into());
1587        } else {
1588            assert!(!consume_gpu_spawn_events);
1589        }
1590
1591        let label = format!("hanabi:pipeline:init_{hash:016X}");
1592        trace!(
1593            "-> creating pipeline '{}' with shader defs:{}",
1594            label,
1595            shader_defs
1596                .iter()
1597                .fold(String::new(), |acc, x| acc + &format!(" {x:?}"))
1598        );
1599
1600        ComputePipelineDescriptor {
1601            label: Some(label.into()),
1602            layout: vec![
1603                self.sim_params_layout_desc.clone(),
1604                key.particle_bind_group_layout_desc.clone(),
1605                key.spawner_bind_group_layout_desc.clone(),
1606                key.metadata_bind_group_layout_desc.clone(),
1607            ],
1608            shader: key.shader,
1609            shader_defs,
1610            entry_point: Some("main".into()),
1611            push_constant_ranges: vec![],
1612            zero_initialize_workgroup_memory: false,
1613        }
1614    }
1615}
1616
1617#[derive(Resource)]
1618pub(crate) struct ParticlesUpdatePipeline {
1619    sim_params_layout_desc: BindGroupLayoutDescriptor,
1620}
1621
1622impl Default for ParticlesUpdatePipeline {
1623    fn default() -> Self {
1624        trace!("GpuSimParams: min_size={}", GpuSimParams::min_size());
1625        let sim_params_layout_desc = BindGroupLayoutDescriptor::new(
1626            "hanabi:bind_group_layout:vfx_update:sim_params@0",
1627            &[
1628                // @group(0) @binding(0) var<uniform> sim_params : SimParams;
1629                BindGroupLayoutEntry {
1630                    binding: 0,
1631                    visibility: ShaderStages::COMPUTE,
1632                    ty: BindingType::Buffer {
1633                        ty: BufferBindingType::Uniform,
1634                        has_dynamic_offset: false,
1635                        min_binding_size: Some(GpuSimParams::min_size()),
1636                    },
1637                    count: None,
1638                },
1639                // @group(0) @binding(1) var<storage, read_write> draw_indirect_buffer :
1640                // array<DrawIndexedIndirectArgs>;
1641                BindGroupLayoutEntry {
1642                    binding: 1,
1643                    visibility: ShaderStages::COMPUTE,
1644                    ty: BindingType::Buffer {
1645                        ty: BufferBindingType::Storage { read_only: false },
1646                        has_dynamic_offset: false,
1647                        min_binding_size: Some(GpuDrawIndexedIndirectArgs::SHADER_SIZE),
1648                    },
1649                    count: None,
1650                },
1651            ],
1652        );
1653
1654        Self {
1655            sim_params_layout_desc,
1656        }
1657    }
1658}
1659
1660#[derive(Debug, Clone, Hash, PartialEq, Eq)]
1661pub(crate) struct ParticleUpdatePipelineKey {
1662    /// Compute shader, with snippets applied, but not preprocessed yet.
1663    shader: Handle<Shader>,
1664    /// Particle layout.
1665    particle_layout: ParticleLayout,
1666    /// Minimum binding size in bytes for the particle layout buffer of the
1667    /// parent effect, if any.
1668    /// Key: READ_PARENT_PARTICLE
1669    parent_particle_layout_min_binding_size: Option<NonZeroU32>,
1670    /// Key: EMITS_GPU_SPAWN_EVENTS
1671    num_event_buffers: u32,
1672    /// Layout of the particle@1 bind group this pipeline was specialized with.
1673    particle_bind_group_layout_desc: BindGroupLayoutDescriptor,
1674    /// Layout of the spawner@2 bind group this pipeline was specialized with.
1675    spawner_bind_group_layout_desc: BindGroupLayoutDescriptor,
1676    /// Layout of the metadata@3 bind group this pipeline was specialized with.
1677    metadata_bind_group_layout_desc: BindGroupLayoutDescriptor,
1678}
1679
1680impl SpecializedComputePipeline for ParticlesUpdatePipeline {
1681    type Key = ParticleUpdatePipelineKey;
1682
1683    fn specialize(&self, key: Self::Key) -> ComputePipelineDescriptor {
1684        // We use the hash to correlate the key content with the GPU resource name
1685        let hash = calc_hash(&key);
1686        trace!("Specializing update pipeline {hash:016X} with key {key:?}");
1687
1688        let mut shader_defs = Vec::with_capacity(6);
1689        shader_defs.push("EM_MAX_SPAWN_ATOMIC".into());
1690        // ChildInfo needs atomic event_count because all threads append to the event
1691        // buffer(s) in parallel.
1692        shader_defs.push("CHILD_INFO_EVENT_COUNT_IS_ATOMIC".into());
1693        if key.particle_layout.contains(Attribute::PREV) {
1694            shader_defs.push("ATTRIBUTE_PREV".into());
1695        }
1696        if key.particle_layout.contains(Attribute::NEXT) {
1697            shader_defs.push("ATTRIBUTE_NEXT".into());
1698        }
1699        if key.parent_particle_layout_min_binding_size.is_some() {
1700            shader_defs.push("READ_PARENT_PARTICLE".into());
1701        }
1702        if key.num_event_buffers > 0 {
1703            shader_defs.push("EMITS_GPU_SPAWN_EVENTS".into());
1704        }
1705
1706        let hash = calc_func_id(&key);
1707        let label = format!("hanabi:pipeline:update_{hash:016X}");
1708        trace!(
1709            "-> creating pipeline '{}' with shader defs:{}",
1710            label,
1711            shader_defs
1712                .iter()
1713                .fold(String::new(), |acc, x| acc + &format!(" {x:?}"))
1714        );
1715
1716        ComputePipelineDescriptor {
1717            label: Some(label.into()),
1718            layout: vec![
1719                self.sim_params_layout_desc.clone(),
1720                key.particle_bind_group_layout_desc.clone(),
1721                key.spawner_bind_group_layout_desc.clone(),
1722                key.metadata_bind_group_layout_desc.clone(),
1723            ],
1724            shader: key.shader,
1725            shader_defs,
1726            entry_point: Some("main".into()),
1727            push_constant_ranges: Vec::new(),
1728            zero_initialize_workgroup_memory: false,
1729        }
1730    }
1731}
1732
1733#[derive(Resource)]
1734pub(crate) struct ParticlesRenderPipeline {
1735    render_device: RenderDevice,
1736    view_layout_desc: BindGroupLayoutDescriptor,
1737    material_layout_descs: HashMap<TextureLayout, BindGroupLayoutDescriptor>,
1738}
1739
1740impl ParticlesRenderPipeline {
1741    /// Cache a material, creating its bind group layout based on the texture
1742    /// layout.
1743    pub fn cache_material(&mut self, layout: &TextureLayout) {
1744        if layout.layout.is_empty() {
1745            return;
1746        }
1747
1748        // FIXME - no current stable API to insert an entry into a HashMap only if it
1749        // doesn't exist, and without having to build a key (as opposed to a reference).
1750        // So do 2 lookups instead, to avoid having to clone the layout if it's already
1751        // cached (which should be the common case).
1752        if self.material_layout_descs.contains_key(layout) {
1753            return;
1754        }
1755
1756        let mut entries = Vec::with_capacity(layout.layout.len() * 2);
1757        let mut index = 0;
1758        for _slot in &layout.layout {
1759            entries.push(BindGroupLayoutEntry {
1760                binding: index,
1761                visibility: ShaderStages::FRAGMENT,
1762                ty: BindingType::Texture {
1763                    multisampled: false,
1764                    sample_type: TextureSampleType::Float { filterable: true },
1765                    view_dimension: TextureViewDimension::D2,
1766                },
1767                count: None,
1768            });
1769            entries.push(BindGroupLayoutEntry {
1770                binding: index + 1,
1771                visibility: ShaderStages::FRAGMENT,
1772                ty: BindingType::Sampler(SamplerBindingType::Filtering),
1773                count: None,
1774            });
1775            index += 2;
1776        }
1777        debug!(
1778            "Creating material bind group with {} entries [{:?}] for layout {:?}",
1779            entries.len(),
1780            entries,
1781            layout
1782        );
1783        let material_bind_group_layout_desc =
1784            BindGroupLayoutDescriptor::new("hanabi:material_layout_render", &entries[..]);
1785        self.material_layout_descs
1786            .insert(layout.clone(), material_bind_group_layout_desc);
1787    }
1788
1789    /// Retrieve a bind group layout for a cached material.
1790    pub fn get_material(&self, layout: &TextureLayout) -> Option<&BindGroupLayoutDescriptor> {
1791        // Prevent a hash and lookup for the trivial case of an empty layout
1792        if layout.layout.is_empty() {
1793            return None;
1794        }
1795
1796        self.material_layout_descs.get(layout)
1797    }
1798}
1799
1800impl FromWorld for ParticlesRenderPipeline {
1801    fn from_world(world: &mut World) -> Self {
1802        let render_device = world.get_resource::<RenderDevice>().unwrap();
1803
1804        let view_layout_desc = BindGroupLayoutDescriptor::new(
1805            "hanabi:bind_group_layout:render:view@0",
1806            &[
1807                // @group(0) @binding(0) var<uniform> view: View;
1808                BindGroupLayoutEntry {
1809                    binding: 0,
1810                    visibility: ShaderStages::VERTEX_FRAGMENT,
1811                    ty: BindingType::Buffer {
1812                        ty: BufferBindingType::Uniform,
1813                        has_dynamic_offset: true,
1814                        min_binding_size: Some(ViewUniform::min_size()),
1815                    },
1816                    count: None,
1817                },
1818                // @group(0) @binding(1) var<uniform> sim_params : SimParams;
1819                BindGroupLayoutEntry {
1820                    binding: 1,
1821                    visibility: ShaderStages::VERTEX_FRAGMENT,
1822                    ty: BindingType::Buffer {
1823                        ty: BufferBindingType::Uniform,
1824                        has_dynamic_offset: false,
1825                        min_binding_size: Some(GpuSimParams::min_size()),
1826                    },
1827                    count: None,
1828                },
1829            ],
1830        );
1831
1832        Self {
1833            render_device: render_device.clone(),
1834            view_layout_desc,
1835            material_layout_descs: default(),
1836        }
1837    }
1838}
1839
1840#[cfg(all(feature = "2d", feature = "3d"))]
1841#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
1842enum PipelineMode {
1843    Camera2d,
1844    Camera3d,
1845}
1846
1847#[derive(Debug, Clone, Hash, PartialEq, Eq)]
1848pub(crate) struct ParticleRenderPipelineKey {
1849    /// Render shader, with snippets applied, but not preprocessed yet.
1850    shader: Handle<Shader>,
1851    /// Particle layout.
1852    particle_layout: ParticleLayout,
1853    mesh_layout: Option<MeshVertexBufferLayoutRef>,
1854    /// Texture layout.
1855    texture_layout: TextureLayout,
1856    /// Key: LOCAL_SPACE_SIMULATION
1857    /// The effect is simulated in local space, and during rendering all
1858    /// particles are transformed by the effect's [`GlobalTransform`].
1859    local_space_simulation: bool,
1860    /// Key: USE_ALPHA_MASK, OPAQUE
1861    /// The particle's alpha masking behavior.
1862    alpha_mask: ParticleRenderAlphaMaskPipelineKey,
1863    /// The effect needs Alpha blend.
1864    alpha_mode: AlphaMode,
1865    /// Key: FLIPBOOK
1866    /// The effect is rendered with flipbook texture animation based on the
1867    /// sprite index of each particle.
1868    flipbook: bool,
1869    /// Key: NEEDS_UV
1870    /// The effect needs UVs.
1871    needs_uv: bool,
1872    /// Key: NEEDS_NORMAL
1873    /// The effect needs normals.
1874    needs_normal: bool,
1875    /// Key: NEEDS_PARTICLE_IN_FRAGMENT
1876    /// The effect needs access to the particle index and buffer in the fragment
1877    /// shader.
1878    needs_particle_fragment: bool,
1879    /// Key: RIBBONS
1880    /// The effect has ribbons.
1881    ribbons: bool,
1882    /// For dual-mode configurations only, the actual mode of the current render
1883    /// pipeline. Otherwise the mode is implicitly determined by the active
1884    /// feature.
1885    #[cfg(all(feature = "2d", feature = "3d"))]
1886    pipeline_mode: PipelineMode,
1887    /// MSAA sample count.
1888    msaa_samples: u32,
1889    /// Is the camera using an HDR render target?
1890    hdr: bool,
1891}
1892
1893#[derive(Clone, Copy, Default, Hash, PartialEq, Eq, Debug)]
1894pub(crate) enum ParticleRenderAlphaMaskPipelineKey {
1895    #[default]
1896    Blend,
1897    /// Key: USE_ALPHA_MASK
1898    /// The effect is rendered with alpha masking.
1899    AlphaMask,
1900    /// Key: OPAQUE
1901    /// The effect is rendered fully-opaquely.
1902    Opaque,
1903}
1904
1905impl Default for ParticleRenderPipelineKey {
1906    fn default() -> Self {
1907        Self {
1908            shader: Handle::default(),
1909            particle_layout: ParticleLayout::empty(),
1910            mesh_layout: None,
1911            texture_layout: default(),
1912            local_space_simulation: false,
1913            alpha_mask: default(),
1914            alpha_mode: AlphaMode::Blend,
1915            flipbook: false,
1916            needs_uv: false,
1917            needs_normal: false,
1918            needs_particle_fragment: false,
1919            ribbons: false,
1920            #[cfg(all(feature = "2d", feature = "3d"))]
1921            pipeline_mode: PipelineMode::Camera3d,
1922            msaa_samples: Msaa::default().samples(),
1923            hdr: false,
1924        }
1925    }
1926}
1927
1928impl SpecializedRenderPipeline for ParticlesRenderPipeline {
1929    type Key = ParticleRenderPipelineKey;
1930
1931    fn specialize(&self, key: Self::Key) -> RenderPipelineDescriptor {
1932        trace!("Specializing render pipeline for key: {key:?}");
1933
1934        trace!("Creating layout for bind group particle@1 of render pass");
1935        let alignment = self
1936            .render_device
1937            .limits()
1938            .min_storage_buffer_offset_alignment;
1939        let spawner_min_binding_size = GpuSpawnerParams::aligned_size(alignment);
1940        let entries = [
1941            // @group(1) @binding(0) var<storage, read> particle_buffer : ParticleBuffer;
1942            BindGroupLayoutEntry {
1943                binding: 0,
1944                visibility: ShaderStages::VERTEX_FRAGMENT,
1945                ty: BindingType::Buffer {
1946                    ty: BufferBindingType::Storage { read_only: true },
1947                    has_dynamic_offset: false,
1948                    min_binding_size: Some(key.particle_layout.min_binding_size()),
1949                },
1950                count: None,
1951            },
1952            // @group(1) @binding(1) var<storage, read> indirect_buffer : IndirectBuffer;
1953            BindGroupLayoutEntry {
1954                binding: 1,
1955                visibility: ShaderStages::VERTEX,
1956                ty: BindingType::Buffer {
1957                    ty: BufferBindingType::Storage { read_only: true },
1958                    has_dynamic_offset: false,
1959                    min_binding_size: Some(NonZeroU64::new(INDIRECT_INDEX_SIZE as u64).unwrap()),
1960                },
1961                count: None,
1962            },
1963            // @group(1) @binding(2) var<storage, read> spawner : Spawner;
1964            BindGroupLayoutEntry {
1965                binding: 2,
1966                visibility: ShaderStages::VERTEX,
1967                ty: BindingType::Buffer {
1968                    ty: BufferBindingType::Storage { read_only: true },
1969                    has_dynamic_offset: true,
1970                    min_binding_size: Some(spawner_min_binding_size),
1971                },
1972                count: None,
1973            },
1974        ];
1975        let particle_bind_group_layout_desc = BindGroupLayoutDescriptor::new(
1976            "hanabi:bind_group_layout:render:particle@1",
1977            &entries[..],
1978        );
1979
1980        let mut layout = vec![
1981            self.view_layout_desc.clone(),
1982            particle_bind_group_layout_desc,
1983        ];
1984        let mut shader_defs = vec![];
1985
1986        let vertex_buffer_layout = key.mesh_layout.as_ref().and_then(|mesh_layout| {
1987            mesh_layout
1988                .0
1989                .get_layout(&[
1990                    Mesh::ATTRIBUTE_POSITION.at_shader_location(0),
1991                    Mesh::ATTRIBUTE_UV_0.at_shader_location(1),
1992                    Mesh::ATTRIBUTE_NORMAL.at_shader_location(2),
1993                ])
1994                .ok()
1995        });
1996
1997        if let Some(material_bind_group_layout) = self.get_material(&key.texture_layout) {
1998            layout.push(material_bind_group_layout.clone());
1999        }
2000
2001        // Key: LOCAL_SPACE_SIMULATION
2002        if key.local_space_simulation {
2003            shader_defs.push("LOCAL_SPACE_SIMULATION".into());
2004        }
2005
2006        match key.alpha_mask {
2007            ParticleRenderAlphaMaskPipelineKey::Blend => {}
2008            ParticleRenderAlphaMaskPipelineKey::AlphaMask => {
2009                // Key: USE_ALPHA_MASK
2010                shader_defs.push("USE_ALPHA_MASK".into())
2011            }
2012            ParticleRenderAlphaMaskPipelineKey::Opaque => {
2013                // Key: OPAQUE
2014                shader_defs.push("OPAQUE".into())
2015            }
2016        }
2017
2018        // Key: FLIPBOOK
2019        if key.flipbook {
2020            shader_defs.push("FLIPBOOK".into());
2021        }
2022
2023        // Key: NEEDS_UV
2024        if key.needs_uv {
2025            shader_defs.push("NEEDS_UV".into());
2026        }
2027
2028        // Key: NEEDS_NORMAL
2029        if key.needs_normal {
2030            shader_defs.push("NEEDS_NORMAL".into());
2031        }
2032
2033        if key.needs_particle_fragment {
2034            shader_defs.push("NEEDS_PARTICLE_FRAGMENT".into());
2035        }
2036
2037        // Key: RIBBONS
2038        if key.ribbons {
2039            shader_defs.push("RIBBONS".into());
2040        }
2041
2042        #[cfg(feature = "2d")]
2043        let depth_stencil_2d = DepthStencilState {
2044            format: CORE_2D_DEPTH_FORMAT,
2045            // Use depth buffer with alpha-masked particles, not with transparent ones
2046            depth_write_enabled: false, // TODO - opaque/alphamask 2d
2047            // Bevy uses reverse-Z, so GreaterEqual really means closer
2048            depth_compare: CompareFunction::GreaterEqual,
2049            stencil: StencilState::default(),
2050            bias: DepthBiasState::default(),
2051        };
2052
2053        #[cfg(feature = "3d")]
2054        let depth_stencil_3d = DepthStencilState {
2055            format: CORE_3D_DEPTH_FORMAT,
2056            // Use depth buffer with alpha-masked or opaque particles, not
2057            // with transparent ones
2058            depth_write_enabled: matches!(
2059                key.alpha_mask,
2060                ParticleRenderAlphaMaskPipelineKey::AlphaMask
2061                    | ParticleRenderAlphaMaskPipelineKey::Opaque
2062            ),
2063            // Bevy uses reverse-Z, so GreaterEqual really means closer
2064            depth_compare: CompareFunction::GreaterEqual,
2065            stencil: StencilState::default(),
2066            bias: DepthBiasState::default(),
2067        };
2068
2069        #[cfg(all(feature = "2d", feature = "3d"))]
2070        assert_eq!(CORE_2D_DEPTH_FORMAT, CORE_3D_DEPTH_FORMAT);
2071        #[cfg(all(feature = "2d", feature = "3d"))]
2072        let depth_stencil = match key.pipeline_mode {
2073            PipelineMode::Camera2d => Some(depth_stencil_2d),
2074            PipelineMode::Camera3d => Some(depth_stencil_3d),
2075        };
2076
2077        #[cfg(all(feature = "2d", not(feature = "3d")))]
2078        let depth_stencil = Some(depth_stencil_2d);
2079
2080        #[cfg(all(feature = "3d", not(feature = "2d")))]
2081        let depth_stencil = Some(depth_stencil_3d);
2082
2083        let format = if key.hdr {
2084            ViewTarget::TEXTURE_FORMAT_HDR
2085        } else {
2086            TextureFormat::bevy_default()
2087        };
2088
2089        let hash = calc_func_id(&key);
2090        let label = format!("hanabi:pipeline:render_{hash:016X}");
2091        trace!(
2092            "-> creating pipeline '{}' with shader defs:{}",
2093            label,
2094            shader_defs
2095                .iter()
2096                .fold(String::new(), |acc, x| acc + &format!(" {x:?}"))
2097        );
2098
2099        RenderPipelineDescriptor {
2100            label: Some(label.into()),
2101            vertex: VertexState {
2102                shader: key.shader.clone(),
2103                entry_point: Some("vertex".into()),
2104                shader_defs: shader_defs.clone(),
2105                buffers: vec![vertex_buffer_layout.expect("Vertex buffer layout not present")],
2106            },
2107            fragment: Some(FragmentState {
2108                shader: key.shader,
2109                shader_defs,
2110                entry_point: Some("fragment".into()),
2111                targets: vec![Some(ColorTargetState {
2112                    format,
2113                    blend: Some(key.alpha_mode.into()),
2114                    write_mask: ColorWrites::ALL,
2115                })],
2116            }),
2117            layout,
2118            primitive: PrimitiveState {
2119                front_face: FrontFace::Ccw,
2120                cull_mode: None,
2121                unclipped_depth: false,
2122                polygon_mode: PolygonMode::Fill,
2123                conservative: false,
2124                topology: PrimitiveTopology::TriangleList,
2125                strip_index_format: None,
2126            },
2127            depth_stencil,
2128            multisample: MultisampleState {
2129                count: key.msaa_samples,
2130                mask: !0,
2131                alpha_to_coverage_enabled: false,
2132            },
2133            push_constant_ranges: Vec::new(),
2134            zero_initialize_workgroup_memory: false,
2135        }
2136    }
2137}
2138
2139/// A single effect instance extracted from a [`ParticleEffect`] as a
2140/// render world item.
2141///
2142/// [`ParticleEffect`]: crate::ParticleEffect
2143#[derive(Debug, Clone, PartialEq, Component)]
2144#[require(CachedPipelines, CachedReadyState, CachedEffectMetadata)]
2145pub(crate) struct ExtractedEffect {
2146    /// Handle to the effect asset this instance is based on.
2147    /// The handle is weak to prevent refcount cycles and gracefully handle
2148    /// assets unloaded or destroyed after a draw call has been submitted.
2149    pub handle: Handle<EffectAsset>,
2150    /// Particle layout for the effect.
2151    pub particle_layout: ParticleLayout,
2152    /// Effect capacity, in number of particles.
2153    pub capacity: u32,
2154    /// Layout flags.
2155    pub layout_flags: LayoutFlags,
2156    /// Texture layout.
2157    pub texture_layout: TextureLayout,
2158    /// Textures.
2159    pub textures: Vec<Handle<Image>>,
2160    /// Alpha mode.
2161    pub alpha_mode: AlphaMode,
2162    /// Effect shaders.
2163    pub effect_shaders: EffectShader,
2164    /// Condition under which the effect is simulated.
2165    pub simulation_condition: SimulationCondition,
2166}
2167
2168/// Extracted data for the [`GpuSpawnerParams`].
2169///
2170/// This contains all data which may change each frame during the regular usage
2171/// of the effect, but doesn't require any particular GPU resource update
2172/// (except re-uploading that new data to GPU, of course).
2173#[derive(Debug, Clone, PartialEq, Component)]
2174pub(crate) struct ExtractedSpawner {
2175    /// Number of particles to spawn this frame.
2176    ///
2177    /// This is ignored if the effect is a child effect consuming GPU spawn
2178    /// events.
2179    pub spawn_count: u32,
2180    /// PRNG seed.
2181    pub prng_seed: u32,
2182    /// Global transform of the effect origin.
2183    pub transform: GlobalTransform,
2184    /// Is the effect visible this frame?
2185    pub is_visible: bool,
2186}
2187
2188/// Cache info for the metadata of the effect.
2189///
2190/// This manages the GPU allocation of the [`GpuEffectMetadata`] for this
2191/// effect.
2192#[derive(Debug, Default, Component)]
2193pub(crate) struct CachedEffectMetadata {
2194    /// Allocation ID.
2195    pub table_id: BufferTableId,
2196    /// Current metadata values, cached on CPU for change detection.
2197    pub metadata: GpuEffectMetadata,
2198}
2199
2200/// Extracted parent information for a child effect.
2201///
2202/// This component is present on the [`RenderEntity`] of an extracted effect if
2203/// the effect has a parent effect. Otherwise, it's removed.
2204///
2205/// This components forms an ECS relationship with [`ChildrenEffects`].
2206#[derive(Debug, Clone, Copy, PartialEq, Eq, Component)]
2207#[relationship(relationship_target = ChildrenEffects)]
2208pub(crate) struct ChildEffectOf {
2209    /// Render entity of the parent.
2210    pub parent: Entity,
2211}
2212
2213/// Extracted children information for a parent effect.
2214///
2215/// This component is present on the [`RenderEntity`] of an extracted effect if
2216/// the effect is a parent effect for one or more child effects. Otherwise, it's
2217/// removed.
2218///
2219/// This components forms an ECS relationship with [`ChildEffectOf`]. Note that
2220/// we don't use `linked_spawn` because:
2221/// 1. This would fight with the `SyncToRenderWorld` as the main world
2222///    parent-child hierarchy is by design not an ECS relationship (it's a lose
2223///    declarative coupling).
2224/// 2. The components on the render entity often store GPU resources or other
2225///    data we need to clean-up manually, and not all of them currently use
2226///    lifecycle hooks, so we want to manage despawning manually to prevent
2227///    leaks.
2228#[derive(Debug, Clone, PartialEq, Eq, Component)]
2229#[relationship_target(relationship = ChildEffectOf)]
2230pub(crate) struct ChildrenEffects(Vec<Entity>);
2231
2232impl<'a> IntoIterator for &'a ChildrenEffects {
2233    type Item = <Self::IntoIter as Iterator>::Item;
2234
2235    type IntoIter = std::slice::Iter<'a, Entity>;
2236
2237    #[inline(always)]
2238    fn into_iter(self) -> Self::IntoIter {
2239        self.0.iter()
2240    }
2241}
2242
2243impl Deref for ChildrenEffects {
2244    type Target = [Entity];
2245
2246    fn deref(&self) -> &Self::Target {
2247        &self.0
2248    }
2249}
2250
2251/// Extracted data for an effect's properties, if any.
2252///
2253/// This component is present on the [`RenderEntity`] of an extracted effect if
2254/// that effect has properties. It optionally contains new CPU data to
2255/// (re-)upload this frame. If the effect has no property, this component is
2256/// removed.
2257#[derive(Debug, Component)]
2258pub(crate) struct ExtractedProperties {
2259    /// Property layout for the effect.
2260    pub property_layout: PropertyLayout,
2261    /// Values of properties written in a binary blob according to
2262    /// [`property_layout`].
2263    ///
2264    /// This is `Some(blob)` if the data needs to be (re)uploaded to GPU, or
2265    /// `None` if nothing needs to be done for this frame.
2266    ///
2267    /// [`property_layout`]: crate::render::ExtractedEffect::property_layout
2268    pub property_data: Option<Vec<u8>>,
2269}
2270
2271#[derive(Default, Resource)]
2272pub(crate) struct EffectAssetEvents {
2273    pub images: Vec<AssetEvent<Image>>,
2274}
2275
2276/// System extracting all the asset events for the [`Image`] assets to enable
2277/// dynamic update of images bound to any effect.
2278///
2279/// This system runs in parallel of [`extract_effects`].
2280pub(crate) fn extract_effect_events(
2281    mut events: ResMut<EffectAssetEvents>,
2282    mut image_events: Extract<MessageReader<AssetEvent<Image>>>,
2283) {
2284    #[cfg(feature = "trace")]
2285    let _span = bevy::log::info_span!("extract_effect_events").entered();
2286    trace!("extract_effect_events()");
2287
2288    let EffectAssetEvents { ref mut images } = *events;
2289    *images = image_events.read().copied().collect();
2290}
2291
2292/// Debugging settings.
2293///
2294/// Settings used to debug Hanabi. These have no effect on the actual behavior
2295/// of Hanabi, but may affect its performance.
2296///
2297/// # Example
2298///
2299/// ```
2300/// # use bevy::prelude::*;
2301/// # use bevy_hanabi::*;
2302/// fn startup(mut debug_settings: ResMut<DebugSettings>) {
2303///     // Each time a new effect is spawned, capture 2 frames
2304///     debug_settings.start_capture_on_new_effect = true;
2305///     debug_settings.capture_frame_count = 2;
2306/// }
2307/// ```
2308#[derive(Debug, Default, Clone, Copy, Resource)]
2309pub struct DebugSettings {
2310    /// Enable automatically starting a GPU debugger capture as soon as this
2311    /// frame starts rendering (extract phase).
2312    ///
2313    /// Enable this feature to automatically capture one or more GPU frames when
2314    /// the `extract_effects()` system runs next. This instructs any attached
2315    /// GPU debugger to start a capture; this has no effect if no debugger
2316    /// is attached.
2317    ///
2318    /// If a capture is already on-going this has no effect; the on-going
2319    /// capture needs to be terminated first. Note however that a capture can
2320    /// stop and another start in the same frame.
2321    ///
2322    /// This value is not reset automatically. If you set this to `true`, you
2323    /// should set it back to `false` on next frame to avoid capturing forever.
2324    pub start_capture_this_frame: bool,
2325
2326    /// Enable automatically starting a GPU debugger capture when one or more
2327    /// effects are spawned.
2328    ///
2329    /// Enable this feature to automatically capture one or more GPU frames when
2330    /// a new effect is spawned (as detected by ECS change detection). This
2331    /// instructs any attached GPU debugger to start a capture; this has no
2332    /// effect if no debugger is attached.
2333    pub start_capture_on_new_effect: bool,
2334
2335    /// Number of frames to capture with a GPU debugger.
2336    ///
2337    /// By default this value is zero, and a GPU debugger capture runs for a
2338    /// single frame. If a non-zero frame count is specified here, the capture
2339    /// will instead stop once the specified number of frames has been recorded.
2340    ///
2341    /// You should avoid setting this to a value too large, to prevent the
2342    /// capture size from getting out of control. A typical value is 1 to 3
2343    /// frames, or possibly more (up to 10) for exceptional contexts. Some GPU
2344    /// debuggers or graphics APIs might further limit this value on their own,
2345    /// so there's no guarantee the graphics API will honor this value.
2346    pub capture_frame_count: u32,
2347}
2348
2349#[derive(Debug, Default, Clone, Copy, Resource)]
2350pub(crate) struct RenderDebugSettings {
2351    /// Is a GPU debugger capture on-going?
2352    is_capturing: bool,
2353    /// Start time of any on-going GPU debugger capture.
2354    capture_start: Duration,
2355    /// Number of frames captured so far for on-going GPU debugger capture.
2356    captured_frames: u32,
2357}
2358
2359/// Manage GPU debug capture start/stop.
2360///
2361/// If any GPU debug capture is configured to start or stop in
2362/// [`DebugSettings`], they do so during this system's run. This ensures
2363/// that all GPU commands produced by Hanabi are recorded (but may miss some
2364/// from Bevy itself, if another Bevy system runs before this one).
2365///
2366/// We do this during extract to try and capture as close as possible to an
2367/// entire GPU frame.
2368pub(crate) fn start_stop_gpu_debug_capture(
2369    real_time: Extract<Res<Time<Real>>>,
2370    render_device: Res<RenderDevice>,
2371    debug_settings: Extract<Res<DebugSettings>>,
2372    mut render_debug_settings: ResMut<RenderDebugSettings>,
2373    q_added_effects: Extract<Query<(), Added<CompiledParticleEffect>>>,
2374) {
2375    #[cfg(feature = "trace")]
2376    let _span = bevy::log::info_span!("start_stop_debug_capture").entered();
2377    trace!("start_stop_debug_capture()");
2378
2379    // Stop any pending capture if needed
2380    if render_debug_settings.is_capturing {
2381        render_debug_settings.captured_frames += 1;
2382
2383        if render_debug_settings.captured_frames >= debug_settings.capture_frame_count {
2384            #[expect(unsafe_code, reason = "Debugging only")]
2385            unsafe {
2386                render_device.wgpu_device().stop_graphics_debugger_capture();
2387            }
2388            render_debug_settings.is_capturing = false;
2389            warn!(
2390                "Stopped GPU debug capture after {} frames, at t={}s.",
2391                render_debug_settings.captured_frames,
2392                real_time.elapsed().as_secs_f64()
2393            );
2394        }
2395    }
2396
2397    // If no pending capture, consider starting a new one
2398    if !render_debug_settings.is_capturing
2399        && (debug_settings.start_capture_this_frame
2400            || (debug_settings.start_capture_on_new_effect && !q_added_effects.is_empty()))
2401    {
2402        #[expect(unsafe_code, reason = "Debugging only")]
2403        unsafe {
2404            render_device
2405                .wgpu_device()
2406                .start_graphics_debugger_capture();
2407        }
2408        render_debug_settings.is_capturing = true;
2409        render_debug_settings.capture_start = real_time.elapsed();
2410        render_debug_settings.captured_frames = 0;
2411        warn!(
2412            "Started GPU debug capture of {} frames at t={}s.",
2413            debug_settings.capture_frame_count,
2414            render_debug_settings.capture_start.as_secs_f64()
2415        );
2416    }
2417}
2418
2419/// Write the ready state of all render world effects back into their source
2420/// effect in the main world.
2421pub(crate) fn report_ready_state(
2422    mut main_world: ResMut<MainWorld>,
2423    q_ready_state: Query<&CachedReadyState>,
2424) {
2425    let mut q_effects = main_world.query::<(RenderEntity, &mut CompiledParticleEffect)>();
2426    for (render_entity, mut compiled_particle_effect) in q_effects.iter_mut(&mut main_world) {
2427        if let Ok(cached_ready_state) = q_ready_state.get(render_entity) {
2428            compiled_particle_effect.is_ready = cached_ready_state.is_ready();
2429        }
2430    }
2431}
2432
2433/// System extracting data for rendering of all active [`ParticleEffect`]
2434/// components.
2435///
2436/// [`ParticleEffect`]: crate::ParticleEffect
2437pub(crate) fn extract_effects(
2438    mut commands: Commands,
2439    effects: Extract<Res<Assets<EffectAsset>>>,
2440    default_mesh: Extract<Res<DefaultMesh>>,
2441    // Main world effects to extract
2442    q_effects: Extract<
2443        Query<(
2444            Entity,
2445            RenderEntity,
2446            Option<&InheritedVisibility>,
2447            Option<&ViewVisibility>,
2448            &EffectSpawner,
2449            &CompiledParticleEffect,
2450            Option<Ref<EffectProperties>>,
2451            &GlobalTransform,
2452        )>,
2453    >,
2454    // Render world effects extracted from a previous frame, if any
2455    mut q_extracted_effects: Query<(
2456        &mut ExtractedEffect,
2457        Option<&mut ExtractedSpawner>,
2458        Option<&ChildEffectOf>, // immutable, because of relationship
2459        Option<&mut ExtractedEffectMesh>,
2460        Option<&mut ExtractedProperties>,
2461    )>,
2462) {
2463    #[cfg(feature = "trace")]
2464    let _span = bevy::log::info_span!("extract_effects").entered();
2465    trace!("extract_effects()");
2466
2467    // Loop over all existing effects to extract them
2468    trace!("Extracting {} effects...", q_effects.iter().len());
2469    for (
2470        main_entity,
2471        render_entity,
2472        maybe_inherited_visibility,
2473        maybe_view_visibility,
2474        effect_spawner,
2475        compiled_effect,
2476        maybe_properties,
2477        transform,
2478    ) in q_effects.iter()
2479    {
2480        // Check if shaders are configured
2481        let Some(effect_shaders) = compiled_effect.get_configured_shaders() else {
2482            trace!("Effect {:?}: no configured shader, skipped.", main_entity);
2483            continue;
2484        };
2485
2486        // Check if asset is available, otherwise silently ignore
2487        let Some(asset) = effects.get(&compiled_effect.asset) else {
2488            trace!(
2489                "Effect {:?}: EffectAsset not ready, skipped. asset:{:?}",
2490                main_entity,
2491                compiled_effect.asset
2492            );
2493            continue;
2494        };
2495
2496        let is_visible = maybe_inherited_visibility
2497            .map(|cv| cv.get())
2498            .unwrap_or(true)
2499            && maybe_view_visibility.map(|cv| cv.get()).unwrap_or(true);
2500
2501        let mut cmd = commands.entity(render_entity);
2502
2503        // Fetch the existing extraction compoennts, if any, which we need to update.
2504        // Because we use SyncToRenderWorld, there's always a render entity, but it may
2505        // miss all components. And because we can't query only optional components
2506        // (that would match all entities in the entire world), we force querying
2507        // ExtractedEffect, which means we get a miss if it's the first extraction and
2508        // it's not spawned yet. That's OK, we'll spawn it below.
2509        let (
2510            maybe_extracted_effect,
2511            maybe_extracted_spawner,
2512            maybe_child_of,
2513            maybe_extracted_mesh,
2514            maybe_extracted_properties,
2515        ) = q_extracted_effects
2516            .get_mut(render_entity)
2517            .map(|(extracted_effect, b, c, d, e)| (Some(extracted_effect), b, c, d, e))
2518            .unwrap_or((None, None, None, None, None));
2519
2520        // Extract general effect data
2521        let texture_layout = asset.module().texture_layout();
2522        let layout_flags = compiled_effect.layout_flags;
2523        let alpha_mode = compiled_effect.alpha_mode;
2524        trace!(
2525            "Extracted instance of effect '{}' on entity {:?} (render entity {:?}): texture_layout_count={} texture_count={} layout_flags={:?}",
2526            asset.name,
2527            main_entity,
2528            render_entity,
2529            texture_layout.layout.len(),
2530            compiled_effect.textures.len(),
2531            layout_flags,
2532        );
2533        let new_extracted_effect = ExtractedEffect {
2534            handle: compiled_effect.asset.clone(),
2535            particle_layout: asset.particle_layout().clone(),
2536            capacity: asset.capacity(),
2537            layout_flags,
2538            texture_layout,
2539            textures: compiled_effect.textures.clone(),
2540            alpha_mode,
2541            effect_shaders: effect_shaders.clone(),
2542            simulation_condition: asset.simulation_condition,
2543        };
2544        if let Some(mut extracted_effect) = maybe_extracted_effect {
2545            extracted_effect.set_if_neq(new_extracted_effect);
2546        } else {
2547            trace!(
2548                "Inserting new ExtractedEffect component on {:?}",
2549                render_entity
2550            );
2551            cmd.insert(new_extracted_effect);
2552        }
2553
2554        // Extract the spawner data
2555        let new_spawner = ExtractedSpawner {
2556            spawn_count: effect_spawner.spawn_count,
2557            prng_seed: compiled_effect.prng_seed,
2558            transform: *transform,
2559            is_visible,
2560        };
2561        trace!(
2562            "[Effect {}] spawn_count={} prng_seed={}",
2563            render_entity,
2564            new_spawner.spawn_count,
2565            new_spawner.prng_seed
2566        );
2567        if let Some(mut extracted_spawner) = maybe_extracted_spawner {
2568            extracted_spawner.set_if_neq(new_spawner);
2569        } else {
2570            trace!(
2571                "Inserting new ExtractedSpawner component on {}",
2572                render_entity
2573            );
2574            cmd.insert(new_spawner);
2575        }
2576
2577        // Extract the effect mesh
2578        let mesh = compiled_effect
2579            .mesh
2580            .clone()
2581            .unwrap_or(default_mesh.0.clone());
2582        let new_mesh = ExtractedEffectMesh { mesh: mesh.id() };
2583        if let Some(mut extracted_mesh) = maybe_extracted_mesh {
2584            extracted_mesh.set_if_neq(new_mesh);
2585        } else {
2586            trace!(
2587                "Inserting new ExtractedEffectMesh component on {:?}",
2588                render_entity
2589            );
2590            cmd.insert(new_mesh);
2591        }
2592
2593        // Extract the parent, if any, and resolve its render entity
2594        let parent_render_entity = if let Some(main_entity) = compiled_effect.parent {
2595            let Ok((_, render_entity, _, _, _, _, _, _)) = q_effects.get(main_entity) else {
2596                error!(
2597                    "Failed to resolve render entity of parent with main entity {:?}.",
2598                    main_entity
2599                );
2600                cmd.remove::<ChildEffectOf>();
2601                // TODO - prevent extraction altogether here, instead of just de-parenting?
2602                continue;
2603            };
2604            Some(render_entity)
2605        } else {
2606            None
2607        };
2608        if let Some(render_entity) = parent_render_entity {
2609            let new_child_of = ChildEffectOf {
2610                parent: render_entity,
2611            };
2612            // If there's already an ExtractedParent component, ensure we overwrite only if
2613            // different, to not trigger ECS change detection that we rely on.
2614            if let Some(child_effect_of) = maybe_child_of {
2615                // The relationship makes ChildEffectOf immutable, so re-insert to mutate
2616                if *child_effect_of != new_child_of {
2617                    cmd.insert(new_child_of);
2618                }
2619            } else {
2620                trace!(
2621                    "Inserting new ChildEffectOf component on {:?}",
2622                    render_entity
2623                );
2624                cmd.insert(new_child_of);
2625            }
2626        } else {
2627            cmd.remove::<ChildEffectOf>();
2628        }
2629
2630        // Extract property data
2631        let property_layout = asset.property_layout();
2632        if property_layout.is_empty() {
2633            cmd.remove::<ExtractedProperties>();
2634        } else {
2635            // Re-extract CPU property data if any. Note that this data is not a "new value"
2636            // but instead a "value that must be uploaded this frame", and therefore is
2637            // empty when there's no change (as opposed to, having a constant value
2638            // frame-to-frame).
2639            let property_data = if let Some(properties) = maybe_properties {
2640                if properties.is_changed() {
2641                    trace!("Detected property change, re-serializing...");
2642                    Some(properties.serialize(&property_layout))
2643                } else {
2644                    None
2645                }
2646            } else {
2647                None
2648            };
2649
2650            let new_properties = ExtractedProperties {
2651                property_layout,
2652                property_data,
2653            };
2654            trace!("new_properties = {new_properties:?}");
2655
2656            if let Some(mut extracted_properties) = maybe_extracted_properties {
2657                // Always mutate if there's new CPU data to re-upload. Otherwise check for any
2658                // other change.
2659                if new_properties.property_data.is_some()
2660                    || (extracted_properties.property_layout != new_properties.property_layout)
2661                {
2662                    trace!(
2663                        "Updating existing ExtractedProperties (was: {:?})",
2664                        extracted_properties.as_ref()
2665                    );
2666                    *extracted_properties = new_properties;
2667                }
2668            } else {
2669                trace!(
2670                    "Inserting new ExtractedProperties component on {:?}",
2671                    render_entity
2672                );
2673                cmd.insert(new_properties);
2674            }
2675        }
2676    }
2677}
2678
2679pub(crate) fn extract_sim_params(
2680    real_time: Extract<Res<Time<Real>>>,
2681    virtual_time: Extract<Res<Time<Virtual>>>,
2682    time: Extract<Res<Time<EffectSimulation>>>,
2683    mut sim_params: ResMut<SimParams>,
2684) {
2685    #[cfg(feature = "trace")]
2686    let _span = bevy::log::info_span!("extract_sim_params").entered();
2687    trace!("extract_sim_params()");
2688
2689    // Save simulation params into render world
2690    sim_params.time = time.elapsed_secs_f64();
2691    sim_params.delta_time = time.delta_secs();
2692    sim_params.virtual_time = virtual_time.elapsed_secs_f64();
2693    sim_params.virtual_delta_time = virtual_time.delta_secs();
2694    sim_params.real_time = real_time.elapsed_secs_f64();
2695    sim_params.real_delta_time = real_time.delta_secs();
2696    trace!(
2697        "SimParams: time={} delta_time={} vtime={} delta_vtime={} rtime={} delta_rtime={}",
2698        sim_params.time,
2699        sim_params.delta_time,
2700        sim_params.virtual_time,
2701        sim_params.virtual_delta_time,
2702        sim_params.real_time,
2703        sim_params.real_delta_time,
2704    );
2705}
2706
2707/// Various GPU limits and aligned sizes computed once and cached.
2708struct GpuLimits {
2709    /// Value of [`WgpuLimits::min_storage_buffer_offset_alignment`].
2710    ///
2711    /// [`WgpuLimits::min_storage_buffer_offset_alignment`]: bevy::render::settings::WgpuLimits::min_storage_buffer_offset_alignment
2712    storage_buffer_align: NonZeroU32,
2713
2714    /// Size of [`GpuEffectMetadata`] aligned to the contraint of
2715    /// [`WgpuLimits::min_storage_buffer_offset_alignment`].
2716    ///
2717    /// [`WgpuLimits::min_storage_buffer_offset_alignment`]: bevy::render::settings::WgpuLimits::min_storage_buffer_offset_alignment
2718    effect_metadata_aligned_size: NonZeroU32,
2719}
2720
2721impl GpuLimits {
2722    pub fn from_device(render_device: &RenderDevice) -> Self {
2723        let storage_buffer_align =
2724            render_device.limits().min_storage_buffer_offset_alignment as u64;
2725
2726        let effect_metadata_aligned_size = NonZeroU32::new(
2727            GpuEffectMetadata::min_size()
2728                .get()
2729                .next_multiple_of(storage_buffer_align) as u32,
2730        )
2731        .unwrap();
2732
2733        trace!(
2734            "GPU-aligned sizes (align: {} B):\n- GpuEffectMetadata: {} B -> {} B",
2735            storage_buffer_align,
2736            GpuEffectMetadata::min_size().get(),
2737            effect_metadata_aligned_size.get(),
2738        );
2739
2740        Self {
2741            storage_buffer_align: NonZeroU32::new(storage_buffer_align as u32).unwrap(),
2742            effect_metadata_aligned_size,
2743        }
2744    }
2745
2746    /// Byte alignment for any storage buffer binding.
2747    pub fn storage_buffer_align(&self) -> NonZeroU32 {
2748        self.storage_buffer_align
2749    }
2750
2751    /// Byte offset of the [`GpuEffectMetadata`] of a given buffer.
2752    pub fn effect_metadata_offset(&self, buffer_index: u32) -> u64 {
2753        self.effect_metadata_aligned_size.get() as u64 * buffer_index as u64
2754    }
2755}
2756
2757/// Global render world resource containing the GPU data to draw all the
2758/// particle effects in all views.
2759///
2760/// The resource is populated by [`prepare_effects()`] with all the effects to
2761/// render for the current frame, for all views in the frame, and consumed by
2762/// [`queue_effects()`] to actually enqueue the drawning commands to draw those
2763/// effects.
2764#[derive(Resource)]
2765pub struct EffectsMeta {
2766    /// Bind group for the camera view, containing the camera projection and
2767    /// other uniform values related to the camera.
2768    view_bind_group: Option<BindGroup>,
2769    /// Bind group #0 of the vfx_update shader, for the simulation parameters
2770    /// like the current time and frame delta time.
2771    update_sim_params_bind_group: Option<BindGroup>,
2772    /// Bind group #0 of the vfx_indirect shader, for the simulation parameters
2773    /// like the current time and frame delta time. This is shared with the
2774    /// vfx_init pass too.
2775    indirect_sim_params_bind_group: Option<BindGroup>,
2776    /// Bind group #1 of the vfx_indirect shader, containing both the indirect
2777    /// compute dispatch and render buffers.
2778    indirect_metadata_bind_group: Option<BindGroup>,
2779    /// Bind group #2 of the vfx_indirect shader, containing the spawners.
2780    indirect_spawner_bind_group: Option<BindGroup>,
2781    /// Global shared GPU uniform buffer storing the simulation parameters,
2782    /// uploaded each frame from CPU to GPU.
2783    sim_params_uniforms: UniformBuffer<GpuSimParams>,
2784    /// Global shared GPU buffer storing the various spawner parameter structs
2785    /// for the active effect instances.
2786    spawner_buffer: AlignedBufferVec<GpuSpawnerParams>,
2787    /// Global shared GPU buffer storing the various indirect dispatch structs
2788    /// for the indirect dispatch of the Update pass.
2789    dispatch_indirect_buffer: GpuBuffer<GpuDispatchIndirectArgs>,
2790    /// Global shared GPU buffer storing the various indirect draw structs
2791    /// for the indirect Render pass. Note that we use
2792    /// GpuDrawIndexedIndirectArgs as the largest of the two variants (the
2793    /// other being GpuDrawIndirectArgs). For non-indexed entries, we ignore
2794    /// the last `u32` value.
2795    draw_indirect_buffer: BufferTable<GpuDrawIndexedIndirectArgs>,
2796    /// Global shared GPU buffer storing the various `EffectMetadata`
2797    /// structs for the active effect instances.
2798    effect_metadata_buffer: BufferTable<GpuEffectMetadata>,
2799    /// Various GPU limits and aligned sizes lazily allocated and cached for
2800    /// convenience.
2801    gpu_limits: GpuLimits,
2802    indirect_shader_noevent: Handle<Shader>,
2803    indirect_shader_events: Handle<Shader>,
2804    /// Pipeline cache ID of the two indirect dispatch pass pipelines (the
2805    /// -noevent and -events variants).
2806    indirect_pipeline_ids: [CachedComputePipelineId; 2],
2807    /// Pipeline cache ID of the active indirect dispatch pass pipeline, which
2808    /// is either the -noevent or -events variant depending on whether there's
2809    /// any child effect with GPU events currently active.
2810    active_indirect_pipeline_id: CachedComputePipelineId,
2811}
2812
2813impl EffectsMeta {
2814    pub fn new(
2815        device: RenderDevice,
2816        indirect_shader_noevent: Handle<Shader>,
2817        indirect_shader_events: Handle<Shader>,
2818    ) -> Self {
2819        let gpu_limits = GpuLimits::from_device(&device);
2820
2821        // Ensure individual GpuSpawnerParams elements are properly aligned so they can
2822        // be addressed individually by the computer shaders.
2823        let item_align = gpu_limits.storage_buffer_align();
2824        trace!(
2825            "Aligning storage buffers to {} bytes as device limits requires.",
2826            item_align.get()
2827        );
2828
2829        Self {
2830            view_bind_group: None,
2831            update_sim_params_bind_group: None,
2832            indirect_sim_params_bind_group: None,
2833            indirect_metadata_bind_group: None,
2834            indirect_spawner_bind_group: None,
2835            sim_params_uniforms: UniformBuffer::default(),
2836            spawner_buffer: AlignedBufferVec::new(
2837                BufferUsages::STORAGE,
2838                Some(item_align.into()),
2839                Some("hanabi:buffer:spawner".to_string()),
2840            ),
2841            dispatch_indirect_buffer: GpuBuffer::new(
2842                BufferUsages::STORAGE | BufferUsages::INDIRECT,
2843                Some("hanabi:buffer:dispatch_indirect".to_string()),
2844            ),
2845            draw_indirect_buffer: BufferTable::new(
2846                BufferUsages::STORAGE | BufferUsages::INDIRECT,
2847                Some(GpuDrawIndexedIndirectArgs::SHADER_SIZE),
2848                Some("hanabi:buffer:draw_indirect".to_string()),
2849            ),
2850            effect_metadata_buffer: BufferTable::new(
2851                BufferUsages::STORAGE | BufferUsages::INDIRECT,
2852                Some(item_align.into()),
2853                Some("hanabi:buffer:effect_metadata".to_string()),
2854            ),
2855            gpu_limits,
2856            indirect_shader_noevent,
2857            indirect_shader_events,
2858            indirect_pipeline_ids: [
2859                CachedComputePipelineId::INVALID,
2860                CachedComputePipelineId::INVALID,
2861            ],
2862            active_indirect_pipeline_id: CachedComputePipelineId::INVALID,
2863        }
2864    }
2865
2866    pub fn allocate_spawner(
2867        &mut self,
2868        global_transform: &GlobalTransform,
2869        spawn_count: u32,
2870        prng_seed: u32,
2871        slab_offset: u32,
2872        parent_slab_offset: Option<u32>,
2873        effect_metadata_buffer_table_id: BufferTableId,
2874        maybe_cached_draw_indirect_args: Option<&CachedDrawIndirectArgs>,
2875    ) -> u32 {
2876        let spawner_base = self.spawner_buffer.len() as u32;
2877        let transform = global_transform.to_matrix().into();
2878        let inverse_transform = Mat4::from(
2879            // Inverse the Affine3A first, then convert to Mat4. This is a lot more
2880            // efficient than inversing the Mat4.
2881            global_transform.affine().inverse(),
2882        )
2883        .into();
2884        let spawner_params = GpuSpawnerParams {
2885            transform,
2886            inverse_transform,
2887            spawn: spawn_count as i32,
2888            seed: prng_seed,
2889            effect_metadata_index: effect_metadata_buffer_table_id.0,
2890            draw_indirect_index: maybe_cached_draw_indirect_args
2891                .map(|cdia| cdia.get_row().0)
2892                .unwrap_or_default(),
2893            slab_offset,
2894            parent_slab_offset: parent_slab_offset.unwrap_or(u32::MAX),
2895            ..default()
2896        };
2897        trace!("spawner params = {:?}", spawner_params);
2898        self.spawner_buffer.push(spawner_params);
2899        spawner_base
2900    }
2901
2902    pub fn allocate_draw_indirect(
2903        &mut self,
2904        draw_args: &AnyDrawIndirectArgs,
2905    ) -> CachedDrawIndirectArgs {
2906        let row = self
2907            .draw_indirect_buffer
2908            .insert(draw_args.bitcast_to_row_entry());
2909        CachedDrawIndirectArgs {
2910            row,
2911            args: *draw_args,
2912        }
2913    }
2914
2915    pub fn update_draw_indirect(&mut self, row_index: &CachedDrawIndirectArgs) {
2916        self.draw_indirect_buffer
2917            .update(row_index.get_row(), row_index.args.bitcast_to_row_entry());
2918    }
2919
2920    pub fn free_draw_indirect(&mut self, row_index: &CachedDrawIndirectArgs) {
2921        self.draw_indirect_buffer.remove(row_index.get_row());
2922    }
2923}
2924
2925bitflags! {
2926    /// Effect flags.
2927    #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
2928    pub struct LayoutFlags: u32 {
2929        /// No flags.
2930        const NONE = 0;
2931        // DEPRECATED - The effect uses an image texture.
2932        //const PARTICLE_TEXTURE = (1 << 0);
2933        /// The effect is simulated in local space.
2934        const LOCAL_SPACE_SIMULATION = (1 << 2);
2935        /// The effect uses alpha masking instead of alpha blending. Only used for 3D.
2936        const USE_ALPHA_MASK = (1 << 3);
2937        /// The effect is rendered with flipbook texture animation based on the
2938        /// [`Attribute::SPRITE_INDEX`] of each particle.
2939        const FLIPBOOK = (1 << 4);
2940        /// The effect needs UVs.
2941        const NEEDS_UV = (1 << 5);
2942        /// The effect has ribbons.
2943        const RIBBONS = (1 << 6);
2944        /// The effects needs normals.
2945        const NEEDS_NORMAL = (1 << 7);
2946        /// The effect is fully-opaque.
2947        const OPAQUE = (1 << 8);
2948        /// The (update) shader emits GPU spawn events to instruct another effect to spawn particles.
2949        const EMIT_GPU_SPAWN_EVENTS = (1 << 9);
2950        /// The (init) shader spawns particles by consuming GPU spawn events, instead of
2951        /// a single CPU spawn count.
2952        const CONSUME_GPU_SPAWN_EVENTS = (1 << 10);
2953        /// The (init or update) shader needs access to its parent particle. This allows
2954        /// a particle init or update pass to read the data of a parent particle, for
2955        /// example to inherit some of the attributes.
2956        const READ_PARENT_PARTICLE = (1 << 11);
2957        /// The effect access to the particle data in the fragment shader.
2958        const NEEDS_PARTICLE_FRAGMENT = (1 << 12);
2959    }
2960}
2961
2962impl Default for LayoutFlags {
2963    fn default() -> Self {
2964        Self::NONE
2965    }
2966}
2967
2968/// Observer raised when the [`CachedEffect`] component is removed, which
2969/// indicates that the effect instance was despawned.
2970pub(crate) fn on_remove_cached_effect(
2971    trigger: On<Remove, CachedEffect>,
2972    query: Query<(
2973        Entity,
2974        &MainEntity,
2975        &CachedEffect,
2976        &DispatchBufferIndices,
2977        Option<&CachedEffectProperties>,
2978        Option<&CachedParentInfo>,
2979        Option<&CachedEffectEvents>,
2980    )>,
2981    mut effect_cache: ResMut<EffectCache>,
2982    mut effect_bind_groups: ResMut<EffectBindGroups>,
2983    mut effects_meta: ResMut<EffectsMeta>,
2984    mut event_cache: ResMut<EventCache>,
2985) {
2986    #[cfg(feature = "trace")]
2987    let _span = bevy::log::info_span!("on_remove_cached_effect").entered();
2988
2989    // FIXME - review this Observer pattern; this triggers for each event one by
2990    // one, which could kill performance if many effects are removed.
2991
2992    // Fecth the components of the effect being destroyed. Note that the despawn
2993    // command above is not yet applied, so this query should always succeed.
2994    let Ok((
2995        render_entity,
2996        main_entity,
2997        cached_effect,
2998        dispatch_buffer_indices,
2999        _opt_props,
3000        _opt_parent,
3001        opt_cached_effect_events,
3002    )) = query.get(trigger.event().entity)
3003    else {
3004        return;
3005    };
3006
3007    // Dealllocate the effect slice in the event buffer, if any.
3008    if let Some(cached_effect_events) = opt_cached_effect_events {
3009        match event_cache.free(cached_effect_events) {
3010            Err(err) => {
3011                error!("Error while freeing effect event slice: {err:?}");
3012            }
3013            Ok(buffer_state) => {
3014                if buffer_state != SlabState::Used {
3015                    // Clear bind groups associated with the old buffer
3016                    effect_bind_groups.init_metadata_bind_groups.clear();
3017                    effect_bind_groups.update_metadata_bind_groups.clear();
3018                }
3019            }
3020        }
3021    }
3022
3023    // Deallocate the effect slice in the GPU effect buffer, and if this was the
3024    // last slice, also deallocate the GPU buffer itself.
3025    trace!(
3026        "=> ParticleEffect on render entity {:?} associated with main entity {:?}, removing...",
3027        render_entity,
3028        main_entity,
3029    );
3030    let Ok(SlabState::Free) = effect_cache.remove(cached_effect) else {
3031        // Buffer was not affected, so all bind groups are still valid. Nothing else to
3032        // do.
3033        return;
3034    };
3035
3036    // Clear bind groups associated with the removed buffer
3037    trace!(
3038        "=> GPU particle slab #{} gone, destroying its bind groups...",
3039        cached_effect.slab_id.index()
3040    );
3041    effect_bind_groups
3042        .particle_slabs
3043        .remove(&cached_effect.slab_id);
3044    effects_meta
3045        .dispatch_indirect_buffer
3046        .free(dispatch_buffer_indices.update_dispatch_indirect_buffer_row_index);
3047}
3048
3049/// Observer raised when the [`CachedEffectMetadata`] component is removed, to
3050/// deallocate the GPU resources associated with the indirect draw args.
3051pub(crate) fn on_remove_cached_metadata(
3052    trigger: On<Remove, CachedEffectMetadata>,
3053    query: Query<&CachedEffectMetadata>,
3054    mut effects_meta: ResMut<EffectsMeta>,
3055) {
3056    #[cfg(feature = "trace")]
3057    let _span = bevy::log::info_span!("on_remove_cached_metadata").entered();
3058
3059    if let Ok(cached_metadata) = query.get(trigger.event().entity) {
3060        if cached_metadata.table_id.is_valid() {
3061            effects_meta
3062                .effect_metadata_buffer
3063                .remove(cached_metadata.table_id);
3064        }
3065    };
3066}
3067
3068/// Observer raised when the [`CachedDrawIndirectArgs`] component is removed, to
3069/// deallocate the GPU resources associated with the indirect draw args.
3070pub(crate) fn on_remove_cached_draw_indirect_args(
3071    trigger: On<Remove, CachedDrawIndirectArgs>,
3072    query: Query<&CachedDrawIndirectArgs>,
3073    mut effects_meta: ResMut<EffectsMeta>,
3074) {
3075    #[cfg(feature = "trace")]
3076    let _span = bevy::log::info_span!("on_remove_cached_draw_indirect_args").entered();
3077
3078    if let Ok(cached_draw_args) = query.get(trigger.event().entity) {
3079        effects_meta.free_draw_indirect(cached_draw_args);
3080    };
3081}
3082
3083/// Clear pending GPU resources left from previous frame.
3084///
3085/// Those generally are source buffers for buffer-to-buffer copies on capacity
3086/// growth, which need the source buffer to be alive until the copy is done,
3087/// then can be discarded here.
3088pub(crate) fn clear_previous_frame_resizes(
3089    mut effects_meta: ResMut<EffectsMeta>,
3090    mut sort_bind_groups: ResMut<SortBindGroups>,
3091    mut init_fill_dispatch_queue: ResMut<InitFillDispatchQueue>,
3092) {
3093    #[cfg(feature = "trace")]
3094    let _span = bevy::log::info_span!("clear_previous_frame_resizes").entered();
3095    trace!("clear_previous_frame_resizes");
3096
3097    init_fill_dispatch_queue.clear();
3098
3099    // Clear last frame's buffer resizes which may have occured during last frame,
3100    // during `Node::run()` while the `BufferTable` could not be mutated. This is
3101    // the first point at which we can do that where we're not blocking the main
3102    // world (so, excluding the extract system).
3103    effects_meta
3104        .dispatch_indirect_buffer
3105        .clear_previous_frame_resizes();
3106    effects_meta
3107        .draw_indirect_buffer
3108        .clear_previous_frame_resizes();
3109    effects_meta
3110        .effect_metadata_buffer
3111        .clear_previous_frame_resizes();
3112    sort_bind_groups.clear_previous_frame_resizes();
3113}
3114
3115// Fixup the [`CachedChildInfo::global_child_index`] once all child infos have
3116// been allocated.
3117pub fn fixup_parents(
3118    q_changed_parents: Query<(Entity, Ref<CachedParentInfo>)>,
3119    mut q_children: Query<&mut CachedChildInfo>,
3120) {
3121    #[cfg(feature = "trace")]
3122    let _span = bevy::log::info_span!("fixup_parents").entered();
3123    trace!("fixup_parents");
3124
3125    // Once all parents are (re-)allocated, fix up the global index of all
3126    // children if the parent base index changed.
3127    trace!(
3128        "Updating the global index of children of parent effects whose child list just changed..."
3129    );
3130    for (parent_entity, cached_parent_info) in q_changed_parents.iter() {
3131        let base_index =
3132            cached_parent_info.byte_range.start / GpuChildInfo::SHADER_SIZE.get() as u32;
3133        let parent_changed = cached_parent_info.is_changed();
3134        trace!(
3135            "Updating {} children of parent effect {:?} with base child index {} (parent_changed:{})...",
3136            cached_parent_info.children.len(),
3137            parent_entity,
3138            base_index,
3139            parent_changed
3140        );
3141        for (child_entity, _) in &cached_parent_info.children {
3142            let Ok(mut cached_child_info) = q_children.get_mut(*child_entity) else {
3143                error!(
3144                    "Cannot find child {:?} declared by parent {:?}",
3145                    *child_entity, parent_entity
3146                );
3147                continue;
3148            };
3149            if !cached_child_info.is_changed() && !parent_changed {
3150                continue;
3151            }
3152            cached_child_info.global_child_index = base_index + cached_child_info.local_child_index;
3153            trace!(
3154                "+ Updated global index for child ID {:?} of parent {:?}: local={}, global={}",
3155                child_entity,
3156                parent_entity,
3157                cached_child_info.local_child_index,
3158                cached_child_info.global_child_index
3159            );
3160        }
3161    }
3162}
3163
3164/// Allocate the GPU resources for all extracted effects.
3165///
3166/// This adds the [`CachedEffect`] component as needed, and update it with the
3167/// allocation in the [`EffectCache`].
3168pub fn allocate_effects(
3169    mut commands: Commands,
3170    mut q_extracted_effects: Query<
3171        (
3172            Entity,
3173            &ExtractedEffect,
3174            Has<ChildEffectOf>,
3175            Option<&mut CachedEffect>,
3176            Has<DispatchBufferIndices>,
3177        ),
3178        Changed<ExtractedEffect>,
3179    >,
3180    mut effect_cache: ResMut<EffectCache>,
3181    mut effects_meta: ResMut<EffectsMeta>,
3182) {
3183    #[cfg(feature = "trace")]
3184    let _span = bevy::log::info_span!("allocate_effects").entered();
3185    trace!("allocate_effects");
3186
3187    for (entity, extracted_effect, has_parent, maybe_cached_effect, has_dispatch_buffer_indices) in
3188        &mut q_extracted_effects
3189    {
3190        // Insert or update the effect into the EffectCache
3191        if let Some(mut cached_effect) = maybe_cached_effect {
3192            trace!("Updating EffectCache entry for entity {entity:?}...");
3193            let _ = effect_cache.remove(cached_effect.as_ref());
3194            *cached_effect = effect_cache.insert(
3195                extracted_effect.handle.clone(),
3196                extracted_effect.capacity,
3197                &extracted_effect.particle_layout,
3198            );
3199        } else {
3200            trace!("Allocating new entry in EffectCache for entity {entity:?}...");
3201            let cached_effect = effect_cache.insert(
3202                extracted_effect.handle.clone(),
3203                extracted_effect.capacity,
3204                &extracted_effect.particle_layout,
3205            );
3206            commands.entity(entity).insert(cached_effect);
3207        }
3208
3209        // Ensure the particle@1 bind group layout exists for the given configuration of
3210        // particle layout. We do this here only for effects without a parent; for those
3211        // with a parent, we'll do it after we resolved that parent.
3212        if !has_parent {
3213            let parent_min_binding_size = None;
3214            effect_cache.ensure_particle_bind_group_layout_desc(
3215                extracted_effect.particle_layout.min_binding_size32(),
3216                parent_min_binding_size,
3217            );
3218        }
3219
3220        // Ensure the metadata@3 bind group layout exists for the init pass.
3221        {
3222            let consume_gpu_spawn_events = extracted_effect
3223                .layout_flags
3224                .contains(LayoutFlags::CONSUME_GPU_SPAWN_EVENTS);
3225            effect_cache.ensure_metadata_init_bind_group_layout_desc(consume_gpu_spawn_events);
3226        }
3227
3228        // Allocate DispatchBufferIndices if not present yet
3229        if !has_dispatch_buffer_indices {
3230            let update_dispatch_indirect_buffer_row_index =
3231                effects_meta.dispatch_indirect_buffer.allocate();
3232            commands.entity(entity).insert(DispatchBufferIndices {
3233                update_dispatch_indirect_buffer_row_index,
3234            });
3235        }
3236    }
3237}
3238
3239/// Update any cached mesh info based on any relocation done by Bevy itself.
3240///
3241/// Bevy will merge small meshes into larger GPU buffers automatically. When
3242/// this happens, the mesh location changes, and we need to update our
3243/// references to it in order to know how to issue the draw commands.
3244///
3245/// This system updates both the [`CachedMeshLocation`] and the
3246/// [`CachedIndirectDrawArgs`] components.
3247pub fn update_mesh_locations(
3248    mut commands: Commands,
3249    mut effects_meta: ResMut<EffectsMeta>,
3250    mesh_allocator: Res<MeshAllocator>,
3251    render_meshes: Res<RenderAssets<RenderMesh>>,
3252    mut q_cached_effects: Query<(
3253        Entity,
3254        &ExtractedEffectMesh,
3255        Option<&mut CachedMeshLocation>,
3256        Option<&mut CachedDrawIndirectArgs>,
3257    )>,
3258) {
3259    #[cfg(feature = "trace")]
3260    let _span = bevy::log::info_span!("update_mesh_locations").entered();
3261    trace!("update_mesh_locations");
3262
3263    for (entity, extracted_mesh, maybe_cached_mesh_location, maybe_cached_draw_indirect_args) in
3264        &mut q_cached_effects
3265    {
3266        let mut cmds = commands.entity(entity);
3267
3268        // Resolve the render mesh
3269        let Some(render_mesh) = render_meshes.get(extracted_mesh.mesh) else {
3270            warn!(
3271                "Cannot find render mesh of particle effect instance on entity {:?}, despite applying default mesh. Invalid asset handle: {:?}",
3272                entity, extracted_mesh.mesh
3273            );
3274            cmds.remove::<CachedMeshLocation>();
3275            continue;
3276        };
3277
3278        // Find the location where the render mesh was allocated. This is handled by
3279        // Bevy itself in the allocate_and_free_meshes() system. Bevy might
3280        // re-batch the vertex and optional index data of meshes together at any point,
3281        // so we need to confirm that the location data we may have cached is still
3282        // valid.
3283        let Some(mesh_vertex_buffer_slice) = mesh_allocator.mesh_vertex_slice(&extracted_mesh.mesh)
3284        else {
3285            trace!(
3286                "Effect main_entity {:?}: cannot find vertex slice of render mesh {:?}",
3287                entity,
3288                extracted_mesh.mesh
3289            );
3290            cmds.remove::<CachedMeshLocation>();
3291            continue;
3292        };
3293        let mesh_index_buffer_slice = mesh_allocator.mesh_index_slice(&extracted_mesh.mesh);
3294        let indexed =
3295            if let RenderMeshBufferInfo::Indexed { index_format, .. } = render_mesh.buffer_info {
3296                if let Some(ref slice) = mesh_index_buffer_slice {
3297                    Some(MeshIndexSlice {
3298                        format: index_format,
3299                        buffer: slice.buffer.clone(),
3300                        range: slice.range.clone(),
3301                    })
3302                } else {
3303                    trace!(
3304                        "Effect main_entity {:?}: cannot find index slice of render mesh {:?}",
3305                        entity,
3306                        extracted_mesh.mesh
3307                    );
3308                    cmds.remove::<CachedMeshLocation>();
3309                    continue;
3310                }
3311            } else {
3312                None
3313            };
3314
3315        // Calculate the new draw args and mesh location based on Bevy's info
3316        let new_draw_args = AnyDrawIndirectArgs::from_slices(
3317            &mesh_vertex_buffer_slice,
3318            mesh_index_buffer_slice.as_ref(),
3319        );
3320        let new_mesh_location = match &mesh_index_buffer_slice {
3321            // Indexed mesh rendering
3322            Some(mesh_index_buffer_slice) => CachedMeshLocation {
3323                vertex_buffer: mesh_vertex_buffer_slice.buffer.id(),
3324                vertex_or_index_count: mesh_index_buffer_slice.range.len() as u32,
3325                first_index_or_vertex_offset: mesh_index_buffer_slice.range.start,
3326                vertex_offset_or_base_instance: mesh_vertex_buffer_slice.range.start as i32,
3327                indexed,
3328            },
3329            // Non-indexed mesh rendering
3330            None => CachedMeshLocation {
3331                vertex_buffer: mesh_vertex_buffer_slice.buffer.id(),
3332                vertex_or_index_count: mesh_vertex_buffer_slice.range.len() as u32,
3333                first_index_or_vertex_offset: mesh_vertex_buffer_slice.range.start,
3334                vertex_offset_or_base_instance: 0,
3335                indexed: None,
3336            },
3337        };
3338
3339        // We don't allocate the draw indirect args ahead of time because we need to
3340        // select the indexed vs. non-indexed buffer. Now that we know whether the mesh
3341        // is indexed, we can allocate it (or reallocate it if indexing mode changed).
3342        if let Some(mut cached_draw_indirect) = maybe_cached_draw_indirect_args {
3343            assert!(cached_draw_indirect.row.is_valid());
3344
3345            // If the GPU draw args changed, re-upload to GPU.
3346            if new_draw_args != cached_draw_indirect.args {
3347                debug!(
3348                    "Indirect draw args changed for asset {:?}\nold:{:?}\nnew:{:?}",
3349                    entity, cached_draw_indirect.args, new_draw_args
3350                );
3351                cached_draw_indirect.args = new_draw_args;
3352                effects_meta.update_draw_indirect(cached_draw_indirect.as_ref());
3353            }
3354        } else {
3355            cmds.insert(effects_meta.allocate_draw_indirect(&new_draw_args));
3356        }
3357
3358        // Compare to any cached data and update if necessary, or insert if missing.
3359        // This will trigger change detection in the ECS, which will in turn trigger
3360        // GpuEffectMetadata re-upload.
3361        if let Some(mut old_mesh_location) = maybe_cached_mesh_location {
3362            if *old_mesh_location != new_mesh_location {
3363                debug!(
3364                    "Mesh location changed for asset {:?}\nold:{:?}\nnew:{:?}",
3365                    entity, old_mesh_location, new_mesh_location
3366                );
3367                *old_mesh_location = new_mesh_location;
3368            }
3369        } else {
3370            cmds.insert(new_mesh_location);
3371        }
3372    }
3373}
3374
3375/// Allocate an entry in the GPU table for any [`CachedEffectMetadata`] missing
3376/// one.
3377///
3378/// This system does NOT take care of (re-)uploading recent CPU data to GPU.
3379/// This is done much later in the frame, after batching and once all data for
3380/// it is ready. But it's necessary to ensure the allocation is determined
3381/// already ahead of time, in order to do batching of contiguous metadata
3382/// blocks (TODO; not currently used, also may end up using binary search in
3383/// shader, in which case we won't need continguous-ness and can maybe remove
3384/// this system).
3385// TODO - consider using observer OnAdd instead?
3386pub fn allocate_metadata(
3387    mut effects_meta: ResMut<EffectsMeta>,
3388    mut q_metadata: Query<&mut CachedEffectMetadata>,
3389) {
3390    for mut metadata in &mut q_metadata {
3391        if !metadata.table_id.is_valid() {
3392            metadata.table_id = effects_meta
3393                .effect_metadata_buffer
3394                .insert(metadata.metadata);
3395        } else {
3396            // Unless this is the first time we allocate the GPU entry (above),
3397            // we should never reach the beginning of this frame
3398            // with a changed metadata which has not
3399            // been re-uploaded last frame.
3400            // NO! We can only detect the change *since last run of THIS system*
3401            // so wont' see that a latter system the data.
3402            // assert!(!metadata.is_changed());
3403        }
3404    }
3405}
3406
3407/// Update the [`CachedParentInfo`] of parent effects and the
3408/// [`CachedChildInfo`] of child effects.
3409pub fn allocate_parent_child_infos(
3410    mut commands: Commands,
3411    mut effect_cache: ResMut<EffectCache>,
3412    mut event_cache: ResMut<EventCache>,
3413    // All extracted child effects. May or may not already have a CachedChildInfo. If not, this
3414    // will be spawned below.
3415    mut q_child_effects: Query<(
3416        Entity,
3417        &ExtractedEffect,
3418        &ChildEffectOf,
3419        &CachedEffectEvents,
3420        Option<&mut CachedChildInfo>,
3421    )>,
3422    // All parent effects from a previous frame (already have CachedParentInfo), which can be
3423    // updated in-place without spawning a new CachedParentInfo.
3424    mut q_parent_effects: Query<(
3425        Entity,
3426        &ExtractedEffect,
3427        &CachedEffect,
3428        &ChildrenEffects,
3429        Option<&mut CachedParentInfo>,
3430    )>,
3431) {
3432    #[cfg(feature = "trace")]
3433    let _span = bevy::log::info_span!("allocate_child_infos").entered();
3434    trace!("allocate_child_infos");
3435
3436    // Loop on all child effects and ensure their CachedChildInfo is up-to-date.
3437    for (child_entity, _, child_effect_of, cached_effect_events, maybe_cached_child_info) in
3438        &mut q_child_effects
3439    {
3440        // Fetch the parent effect
3441        let parent_entity = child_effect_of.parent;
3442        let Ok((_, _, parent_cached_effect, children_effects, _)) =
3443            q_parent_effects.get(parent_entity)
3444        else {
3445            warn!("Unknown parent #{parent_entity:?} on child entity {child_entity:?}, removing CachedChildInfo.");
3446            if maybe_cached_child_info.is_some() {
3447                commands.entity(child_entity).remove::<CachedChildInfo>();
3448            }
3449            continue;
3450        };
3451
3452        // Find the index of this child entity in its parent's storage
3453        let Some(local_child_index) = children_effects.0.iter().position(|e| *e == child_entity)
3454        else {
3455            warn!("Cannot find child entity {child_entity:?} in the children collection of parent entity {parent_entity:?}. Relationship desync?");
3456            if maybe_cached_child_info.is_some() {
3457                commands.entity(child_entity).remove::<CachedChildInfo>();
3458            }
3459            continue;
3460        };
3461        let local_child_index = local_child_index as u32;
3462
3463        // Fetch the effect buffer of the parent effect
3464        let Some(parent_buffer_binding_source) = effect_cache
3465            .get_slab(&parent_cached_effect.slab_id)
3466            .map(|effect_buffer| effect_buffer.max_binding_source())
3467        else {
3468            warn!(
3469                "Unknown parent slab #{} on parent entity {:?}, removing CachedChildInfo.",
3470                parent_cached_effect.slab_id.index(),
3471                parent_entity
3472            );
3473            if maybe_cached_child_info.is_some() {
3474                commands.entity(child_entity).remove::<CachedChildInfo>();
3475            }
3476            continue;
3477        };
3478
3479        let new_cached_child_info = CachedChildInfo {
3480            parent_slab_id: parent_cached_effect.slab_id,
3481            parent_slab_offset: parent_cached_effect.slice.range().start,
3482            parent_particle_layout: parent_cached_effect.slice.particle_layout.clone(),
3483            parent_buffer_binding_source,
3484            local_child_index,
3485            global_child_index: u32::MAX, // fixed up later by fixup_parents()
3486            init_indirect_dispatch_index: cached_effect_events.init_indirect_dispatch_index,
3487        };
3488        if let Some(mut cached_child_info) = maybe_cached_child_info {
3489            if !cached_child_info.is_locally_equal(&new_cached_child_info) {
3490                *cached_child_info = new_cached_child_info;
3491            }
3492        } else {
3493            commands.entity(child_entity).insert(new_cached_child_info);
3494        }
3495    }
3496
3497    // Loop on all parent effects and ensure their CachedParentInfo is up-to-date.
3498    for (parent_entity, parent_extracted_effect, _, children_effects, maybe_cached_parent_info) in
3499        &mut q_parent_effects
3500    {
3501        let parent_min_binding_size = parent_extracted_effect.particle_layout.min_binding_size32();
3502
3503        // Loop over children and gather GpuChildInfo
3504        let mut new_children = Vec::with_capacity(children_effects.0.len());
3505        let mut new_child_infos = Vec::with_capacity(children_effects.0.len());
3506        for child_entity in children_effects.0.iter() {
3507            // Fetch the child's event buffer allocation info
3508            let Ok((_, child_extracted_effect, _, cached_effect_events, _)) =
3509                q_child_effects.get(*child_entity)
3510            else {
3511                warn!("Child entity {child_entity:?} from parent entity {parent_entity:?} didnt't resolve to a child instance. The parent effect cannot be processed.");
3512                if maybe_cached_parent_info.is_some() {
3513                    commands.entity(parent_entity).remove::<CachedParentInfo>();
3514                }
3515                break;
3516            };
3517
3518            // Fetch the GPU event buffer of the child
3519            let Some(event_buffer) = event_cache.get_buffer(cached_effect_events.buffer_index)
3520            else {
3521                warn!("Child entity {child_entity:?} from parent entity {parent_entity:?} doesn't have an allocated GPU event buffer. The parent effect cannot be processed.");
3522                break;
3523            };
3524
3525            let buffer_binding_source = BufferBindingSource {
3526                buffer: event_buffer.clone(),
3527                offset: cached_effect_events.range.start,
3528                size: NonZeroU32::new(cached_effect_events.range.len() as u32).unwrap(),
3529            };
3530            new_children.push((*child_entity, buffer_binding_source));
3531
3532            new_child_infos.push(GpuChildInfo {
3533                event_count: 0,
3534                init_indirect_dispatch_index: cached_effect_events.init_indirect_dispatch_index,
3535            });
3536
3537            // Ensure the particle@1 bind group layout exists for the given configuration of
3538            // particle layout. We do this here only for effects with a parent; for those
3539            // without a parent, we already did this in allocate_effects().
3540            effect_cache.ensure_particle_bind_group_layout_desc(
3541                child_extracted_effect.particle_layout.min_binding_size32(),
3542                Some(parent_min_binding_size),
3543            );
3544        }
3545
3546        // If we don't have all children, just abort this effect. We don't try to have
3547        // partial relationships, this is too complex for shader bindings.
3548        debug_assert_eq!(new_children.len(), new_child_infos.len());
3549        if (new_children.len() < children_effects.len()) && maybe_cached_parent_info.is_some() {
3550            warn!("One or more child effect(s) on parent effect {parent_entity:?} failed to configure. The parent effect cannot be processed.");
3551            commands.entity(parent_entity).remove::<CachedParentInfo>();
3552            continue;
3553        }
3554
3555        // Insert or update the CachedParentInfo component of the parent effect
3556        if let Some(mut cached_parent_info) = maybe_cached_parent_info {
3557            if cached_parent_info.children != new_children {
3558                // FIXME - missing way to just update in-place without changing the allocation
3559                // size!
3560                // if cached_parent_info.children.len() == new_children.len() {
3561                //} else {
3562                event_cache.reallocate_child_infos(
3563                    parent_entity,
3564                    new_children,
3565                    &new_child_infos[..],
3566                    cached_parent_info.as_mut(),
3567                );
3568                //}
3569            }
3570        } else {
3571            let cached_parent_info =
3572                event_cache.allocate_child_infos(parent_entity, new_children, &new_child_infos[..]);
3573            commands.entity(parent_entity).insert(cached_parent_info);
3574        }
3575    }
3576}
3577
3578/// Prepare the init and update compute pipelines for an effect.
3579///
3580/// This caches the pipeline IDs once resolved, and their compiling state when
3581/// it changes, to determine when an effect is ready to be used.
3582///
3583/// Note that we do that proactively even if the effect will be skipped this
3584/// frame (for example because it's not visible). This ensures we queue pipeline
3585/// compilations ASAP, as they can take a long time (10+ frames). We also use
3586/// the pipeline compiling state, which we query here, to inform whether the
3587/// effect is ready for this frame. So in general if this is a new pipeline, it
3588/// won't be ready this frame.
3589pub fn prepare_init_update_pipelines(
3590    mut q_effects: Query<(
3591        Entity,
3592        &ExtractedEffect,
3593        &CachedEffect,
3594        Option<&CachedChildInfo>,
3595        Option<&CachedParentInfo>,
3596        Option<&CachedEffectProperties>,
3597        &mut CachedPipelines,
3598    )>,
3599    // FIXME - need mut for bind group layout creation; shouldn't be create there though
3600    mut effect_cache: ResMut<EffectCache>,
3601    pipeline_cache: Res<PipelineCache>,
3602    property_cache: ResMut<PropertyCache>,
3603    init_pipeline: Res<ParticlesInitPipeline>,
3604    update_pipeline: Res<ParticlesUpdatePipeline>,
3605    mut specialized_init_pipelines: ResMut<SpecializedComputePipelines<ParticlesInitPipeline>>,
3606    mut specialized_update_pipelines: ResMut<SpecializedComputePipelines<ParticlesUpdatePipeline>>,
3607) {
3608    #[cfg(feature = "trace")]
3609    let _span = bevy::log::info_span!("prepare_init_update_pipelines").entered();
3610    trace!("prepare_init_update_pipelines");
3611
3612    // Note: As of Bevy 0.16 we can't evict old pipelines from the cache. They're
3613    // inserted forever. https://github.com/bevyengine/bevy/issues/19925
3614
3615    for (
3616        entity,
3617        extracted_effect,
3618        cached_effect,
3619        maybe_cached_child_info,
3620        maybe_cached_parent_info,
3621        maybe_cached_properties,
3622        mut cached_pipelines,
3623    ) in &mut q_effects
3624    {
3625        trace!(
3626            "Preparing pipelines for effect {:?}... (flags: {:?})",
3627            entity,
3628            cached_pipelines.flags
3629        );
3630
3631        let particle_layout = &cached_effect.slice.particle_layout;
3632        let particle_layout_min_binding_size = particle_layout.min_binding_size32();
3633        let has_event_buffer = maybe_cached_child_info.is_some();
3634        let parent_particle_layout_min_binding_size = maybe_cached_child_info
3635            .as_ref()
3636            .map(|cci| cci.parent_particle_layout.min_binding_size32());
3637
3638        let Some(particle_bind_group_layout_desc) = effect_cache.particle_bind_group_layout_desc(
3639            particle_layout_min_binding_size,
3640            parent_particle_layout_min_binding_size,
3641        ) else {
3642            error!("Failed to find particle sim bind group @1 for min_binding_size={} parent_min_binding_size={:?}",
3643                particle_layout_min_binding_size, parent_particle_layout_min_binding_size);
3644            continue;
3645        };
3646        let particle_bind_group_layout_desc = particle_bind_group_layout_desc.clone();
3647
3648        // This should always exist by the time we reach this point, because we should
3649        // have inserted any property in the cache, which would have allocated the
3650        // proper bind group layout (or the default no-property one).
3651        let property_layout_min_binding_size =
3652            maybe_cached_properties.map(|cp| cp.property_layout.min_binding_size());
3653        let spawner_bind_group_layout_desc = property_cache
3654            .bind_group_layout_desc(property_layout_min_binding_size)
3655            .unwrap_or_else(|| {
3656                panic!(
3657                    "Failed to find spawner@2 bind group layout for property binding size {:?}",
3658                    property_layout_min_binding_size,
3659                )
3660            });
3661        trace!(
3662            "Retrieved spawner@2 bind group layout desc for property binding size {}:  {:?}.",
3663            property_layout_min_binding_size
3664                .as_ref()
3665                .map(|size| size.get())
3666                .unwrap_or(0),
3667            spawner_bind_group_layout_desc,
3668        );
3669
3670        // Resolve the init pipeline
3671        let init_pipeline_id = if let Some(init_pipeline_id) = cached_pipelines.init.as_ref() {
3672            *init_pipeline_id
3673        } else {
3674            // Clear flag just in case, to ensure consistency.
3675            cached_pipelines
3676                .flags
3677                .remove(CachedPipelineFlags::INIT_PIPELINE_READY);
3678
3679            // Fetch the metadata@3 bind group layout from the cache
3680            let metadata_bind_group_layout_desc = effect_cache
3681                .metadata_init_bind_group_layout_desc(has_event_buffer)
3682                .unwrap()
3683                .clone();
3684
3685            let init_pipeline_key_flags = {
3686                let mut flags = ParticleInitPipelineKeyFlags::empty();
3687                flags.set(
3688                    ParticleInitPipelineKeyFlags::ATTRIBUTE_PREV,
3689                    particle_layout.contains(Attribute::PREV),
3690                );
3691                flags.set(
3692                    ParticleInitPipelineKeyFlags::ATTRIBUTE_NEXT,
3693                    particle_layout.contains(Attribute::NEXT),
3694                );
3695                flags.set(
3696                    ParticleInitPipelineKeyFlags::CONSUME_GPU_SPAWN_EVENTS,
3697                    has_event_buffer,
3698                );
3699                flags
3700            };
3701
3702            let init_pipeline_id: CachedComputePipelineId = specialized_init_pipelines.specialize(
3703                pipeline_cache.as_ref(),
3704                &init_pipeline,
3705                ParticleInitPipelineKey {
3706                    shader: extracted_effect.effect_shaders.init.clone(),
3707                    particle_layout_min_binding_size,
3708                    parent_particle_layout_min_binding_size,
3709                    flags: init_pipeline_key_flags,
3710                    particle_bind_group_layout_desc: particle_bind_group_layout_desc.clone(),
3711                    spawner_bind_group_layout_desc: spawner_bind_group_layout_desc.clone(),
3712                    metadata_bind_group_layout_desc,
3713                },
3714            );
3715            trace!("Init pipeline specialized: id={:?}", init_pipeline_id);
3716
3717            cached_pipelines.init = Some(init_pipeline_id);
3718            init_pipeline_id
3719        };
3720
3721        // Resolve the update pipeline
3722        let update_pipeline_id = if let Some(update_pipeline_id) = cached_pipelines.update.as_ref()
3723        {
3724            *update_pipeline_id
3725        } else {
3726            // Clear flag just in case, to ensure consistency.
3727            cached_pipelines
3728                .flags
3729                .remove(CachedPipelineFlags::UPDATE_PIPELINE_READY);
3730
3731            let num_event_buffers = maybe_cached_parent_info
3732                .as_ref()
3733                .map(|p| p.children.len() as u32)
3734                .unwrap_or_default();
3735
3736            // FIXME: currently don't hava a way to determine when this is needed, because
3737            // we know the number of children per parent only after resolving
3738            // all parents, but by that point we forgot if this is a newly added
3739            // effect or not. So since we need to re-ensure for all effects, not
3740            // only new ones, might as well do here...
3741            effect_cache.ensure_metadata_update_bind_group_layout_desc(num_event_buffers);
3742
3743            // Fetch the bind group layouts from the cache
3744            let metadata_bind_group_layout_desc = effect_cache
3745                .metadata_update_bind_group_layout_desc(num_event_buffers)
3746                .unwrap()
3747                .clone();
3748
3749            let update_pipeline_id = specialized_update_pipelines.specialize(
3750                pipeline_cache.as_ref(),
3751                &update_pipeline,
3752                ParticleUpdatePipelineKey {
3753                    shader: extracted_effect.effect_shaders.update.clone(),
3754                    particle_layout: particle_layout.clone(),
3755                    parent_particle_layout_min_binding_size,
3756                    num_event_buffers,
3757                    particle_bind_group_layout_desc: particle_bind_group_layout_desc.clone(),
3758                    spawner_bind_group_layout_desc: spawner_bind_group_layout_desc.clone(),
3759                    metadata_bind_group_layout_desc,
3760                },
3761            );
3762            trace!("Update pipeline specialized: id={:?}", update_pipeline_id);
3763
3764            cached_pipelines.update = Some(update_pipeline_id);
3765            update_pipeline_id
3766        };
3767
3768        // Never batch an effect with a pipeline not available; this will prevent its
3769        // init/update pass from running, but the vfx_indirect pass will run
3770        // nonetheless, which causes desyncs and leads to bugs.
3771        if pipeline_cache
3772            .get_compute_pipeline(init_pipeline_id)
3773            .is_none()
3774        {
3775            trace!(
3776                "Skipping effect from render entity {:?} due to missing or not ready init pipeline (status: {:?})",
3777                entity,
3778                pipeline_cache.get_compute_pipeline_state(init_pipeline_id)
3779            );
3780            cached_pipelines
3781                .flags
3782                .remove(CachedPipelineFlags::INIT_PIPELINE_READY);
3783            continue;
3784        }
3785
3786        // PipelineCache::get_compute_pipeline() only returns a value if the pipeline is
3787        // ready
3788        cached_pipelines
3789            .flags
3790            .insert(CachedPipelineFlags::INIT_PIPELINE_READY);
3791        trace!("[Effect {:?}] Init pipeline ready.", entity);
3792
3793        // Never batch an effect with a pipeline not available; this will prevent its
3794        // init/update pass from running, but the vfx_indirect pass will run
3795        // nonetheless, which causes desyncs and leads to bugs.
3796        if pipeline_cache
3797            .get_compute_pipeline(update_pipeline_id)
3798            .is_none()
3799        {
3800            trace!(
3801                "Skipping effect from render entity {:?} due to missing or not ready update pipeline (status: {:?})",
3802                entity,
3803                pipeline_cache.get_compute_pipeline_state(update_pipeline_id)
3804            );
3805            cached_pipelines
3806                .flags
3807                .remove(CachedPipelineFlags::UPDATE_PIPELINE_READY);
3808            continue;
3809        }
3810
3811        // PipelineCache::get_compute_pipeline() only returns a value if the pipeline is
3812        // ready
3813        cached_pipelines
3814            .flags
3815            .insert(CachedPipelineFlags::UPDATE_PIPELINE_READY);
3816        trace!("[Effect {:?}] Update pipeline ready.", entity);
3817    }
3818}
3819
3820pub fn prepare_indirect_pipeline(
3821    event_cache: Res<EventCache>,
3822    mut effects_meta: ResMut<EffectsMeta>,
3823    pipeline_cache: Res<PipelineCache>,
3824    indirect_pipeline: Res<DispatchIndirectPipeline>,
3825    mut specialized_indirect_pipelines: ResMut<
3826        SpecializedComputePipelines<DispatchIndirectPipeline>,
3827    >,
3828) {
3829    // Ensure the 2 variants of the indirect pipelines are created.
3830    // TODO - move that elsewhere in some one-time setup?
3831    if effects_meta.indirect_pipeline_ids[0] == CachedComputePipelineId::INVALID {
3832        effects_meta.indirect_pipeline_ids[0] = specialized_indirect_pipelines.specialize(
3833            pipeline_cache.as_ref(),
3834            &indirect_pipeline,
3835            DispatchIndirectPipelineKey { has_events: false },
3836        );
3837    }
3838    if effects_meta.indirect_pipeline_ids[1] == CachedComputePipelineId::INVALID {
3839        effects_meta.indirect_pipeline_ids[1] = specialized_indirect_pipelines.specialize(
3840            pipeline_cache.as_ref(),
3841            &indirect_pipeline,
3842            DispatchIndirectPipelineKey { has_events: true },
3843        );
3844    }
3845
3846    // Select the active one depending on whether there's any child info to consume
3847    let is_empty = event_cache.child_infos().is_empty();
3848    if effects_meta.active_indirect_pipeline_id == CachedComputePipelineId::INVALID {
3849        if is_empty {
3850            effects_meta.active_indirect_pipeline_id = effects_meta.indirect_pipeline_ids[0];
3851        } else {
3852            effects_meta.active_indirect_pipeline_id = effects_meta.indirect_pipeline_ids[1];
3853        }
3854    } else {
3855        // If this is the first time we insert an event buffer, we need to switch the
3856        // indirect pass from non-event to event mode. That is, we need to re-allocate
3857        // the pipeline with the child infos buffer binding. Conversely, if there's no
3858        // more effect using GPU spawn events, we can deallocate.
3859        let was_empty =
3860            effects_meta.active_indirect_pipeline_id == effects_meta.indirect_pipeline_ids[0];
3861        if was_empty && !is_empty {
3862            trace!("First event buffer inserted; switching indirect pass to event mode...");
3863            effects_meta.active_indirect_pipeline_id = effects_meta.indirect_pipeline_ids[1];
3864        } else if is_empty && !was_empty {
3865            trace!("Last event buffer removed; switching indirect pass to no-event mode...");
3866            effects_meta.active_indirect_pipeline_id = effects_meta.indirect_pipeline_ids[0];
3867        }
3868    }
3869}
3870
3871// TEMP - Mark all cached effects as invalid for this frame until another system
3872// explicitly marks them as valid. Otherwise we early out in some parts, and
3873// reuse by mistake the previous frame's extraction.
3874pub fn clear_transient_batch_inputs(
3875    mut commands: Commands,
3876    mut q_cached_effects: Query<Entity, With<BatchInput>>,
3877) {
3878    for entity in &mut q_cached_effects {
3879        if let Ok(mut cmd) = commands.get_entity(entity) {
3880            cmd.remove::<BatchInput>();
3881        }
3882    }
3883}
3884
3885/// Effect mesh extracted from the main world.
3886#[derive(Debug, Clone, Copy, PartialEq, Eq, Component)]
3887pub(crate) struct ExtractedEffectMesh {
3888    /// Asset of the effect mesh to draw.
3889    pub mesh: AssetId<Mesh>,
3890}
3891
3892/// Indexed mesh metadata for [`CachedMesh`].
3893#[derive(Debug, Clone)]
3894#[allow(dead_code)]
3895pub(crate) struct MeshIndexSlice {
3896    /// Index format.
3897    pub format: IndexFormat,
3898    /// GPU buffer containing the indices.
3899    pub buffer: Buffer,
3900    /// Range inside [`Self::buffer`] where the indices are.
3901    pub range: Range<u32>,
3902}
3903
3904impl PartialEq for MeshIndexSlice {
3905    fn eq(&self, other: &Self) -> bool {
3906        self.format == other.format
3907            && self.buffer.id() == other.buffer.id()
3908            && self.range == other.range
3909    }
3910}
3911
3912impl Eq for MeshIndexSlice {}
3913
3914/// Cached info about a mesh location in a Bevy buffer. This information is
3915/// uploaded to GPU into [`GpuEffectMetadata`] for indirect rendering, but is
3916/// also kept CPU side in this component to detect when Bevy relocated a mesh,
3917/// so we can invalidate that GPU data.
3918#[derive(Debug, Clone, PartialEq, Eq, Component)]
3919pub(crate) struct CachedMeshLocation {
3920    /// Vertex buffer.
3921    pub vertex_buffer: BufferId,
3922    /// See [`GpuEffectMetadata::vertex_or_index_count`].
3923    pub vertex_or_index_count: u32,
3924    /// See [`GpuEffectMetadata::first_index_or_vertex_offset`].
3925    pub first_index_or_vertex_offset: u32,
3926    /// See [`GpuEffectMetadata::vertex_offset_or_base_instance`].
3927    pub vertex_offset_or_base_instance: i32,
3928    /// Indexed rendering metadata.
3929    pub indexed: Option<MeshIndexSlice>,
3930}
3931
3932bitflags! {
3933    #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
3934    pub struct CachedPipelineFlags: u8 {
3935        const NONE = 0;
3936        /// The init pipeline for this effect is ready for use. This means the compute pipeline is compiled and cached.
3937        const INIT_PIPELINE_READY = (1u8 << 0);
3938        /// The update pipeline for this effect is ready for use. This means the compute pipeline is compiled and cached.
3939        const UPDATE_PIPELINE_READY = (1u8 << 1);
3940    }
3941}
3942
3943impl Default for CachedPipelineFlags {
3944    fn default() -> Self {
3945        Self::NONE
3946    }
3947}
3948
3949/// Render world cached shader pipelines for a [`CachedEffect`].
3950///
3951/// This is updated with the IDs of the pipelines when they are queued for
3952/// compiling, and with the state of those pipelines to detect when the effect
3953/// is ready to be used.
3954///
3955/// This component is always auto-inserted alongside [`ExtractedEffect`] as soon
3956/// as a new effect instance is spawned, because it contains the readiness state
3957/// of those pipelines, which we want to query each frame. The pipelines are
3958/// also mandatory, so this component is always needed.
3959#[derive(Debug, Default, Component)]
3960pub(crate) struct CachedPipelines {
3961    /// Caching flags indicating the pipelines readiness.
3962    pub flags: CachedPipelineFlags,
3963    /// ID of the cached init pipeline. This is valid once the pipeline is
3964    /// queued for compilation, but this doesn't mean the pipeline is ready for
3965    /// use. Readiness is encoded in [`Self::flags`].
3966    pub init: Option<CachedComputePipelineId>,
3967    /// ID of the cached update pipeline. This is valid once the pipeline is
3968    /// queued for compilation, but this doesn't mean the pipeline is ready for
3969    /// use. Readiness is encoded in [`Self::flags`].
3970    pub update: Option<CachedComputePipelineId>,
3971}
3972
3973impl CachedPipelines {
3974    /// Check if all pipelines for this effect are ready.
3975    #[inline]
3976    pub fn is_ready(&self) -> bool {
3977        self.flags.contains(
3978            CachedPipelineFlags::INIT_PIPELINE_READY | CachedPipelineFlags::UPDATE_PIPELINE_READY,
3979        )
3980    }
3981}
3982
3983/// Ready state for this effect.
3984///
3985/// An effect is ready if:
3986/// - Its init and update pipelines are ready, as reported by
3987///   [`CachedPipelines::is_ready()`].
3988///
3989/// This components holds the calculated ready state propagated from all
3990/// ancestor effects, if any. That propagation is done by the
3991/// [`propagate_ready_state()`] system.
3992#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Component)]
3993pub(crate) struct CachedReadyState {
3994    is_ready: bool,
3995}
3996
3997impl CachedReadyState {
3998    #[inline(always)]
3999    pub fn new(is_ready: bool) -> Self {
4000        Self { is_ready }
4001    }
4002
4003    #[inline(always)]
4004    pub fn and(mut self, ancestors_ready: bool) -> Self {
4005        self.and_with(ancestors_ready);
4006        self
4007    }
4008
4009    #[inline(always)]
4010    pub fn and_with(&mut self, ancestors_ready: bool) {
4011        self.is_ready = self.is_ready && ancestors_ready;
4012    }
4013
4014    #[inline(always)]
4015    pub fn is_ready(&self) -> bool {
4016        self.is_ready
4017    }
4018}
4019
4020#[derive(SystemParam)]
4021pub struct PrepareEffectsReadOnlyParams<'w, 's> {
4022    sim_params: Res<'w, SimParams>,
4023    render_device: Res<'w, RenderDevice>,
4024    render_queue: Res<'w, RenderQueue>,
4025    marker: PhantomData<&'s usize>,
4026}
4027
4028/// Update the ready state of all effects, and propagate recursively to
4029/// children.
4030pub(crate) fn propagate_ready_state(
4031    mut q_root_effects: Query<
4032        (
4033            Entity,
4034            Option<&ChildrenEffects>,
4035            Ref<CachedPipelines>,
4036            &mut CachedReadyState,
4037        ),
4038        Without<ChildEffectOf>,
4039    >,
4040    mut orphaned: RemovedComponents<ChildEffectOf>,
4041    q_ready_state: Query<
4042        (
4043            Ref<CachedPipelines>,
4044            &mut CachedReadyState,
4045            Option<&ChildrenEffects>,
4046        ),
4047        With<ChildEffectOf>,
4048    >,
4049    q_child_effects: Query<(Entity, Ref<ChildEffectOf>), With<CachedReadyState>>,
4050    mut orphaned_entities: Local<Vec<Entity>>,
4051) {
4052    #[cfg(feature = "trace")]
4053    let _span = bevy::log::info_span!("propagate_ready_state").entered();
4054    trace!("propagate_ready_state");
4055
4056    // Update orphaned list for this frame, and sort it so we can efficiently binary
4057    // search it
4058    orphaned_entities.clear();
4059    orphaned_entities.extend(orphaned.read());
4060    orphaned_entities.sort_unstable();
4061
4062    // Iterate in parallel over all root effects (those without any parent). This is
4063    // the most common case, so should take care of the heavy lifting of propagating
4064    // to most effects. For child effects, we then descend recursively.
4065    q_root_effects.par_iter_mut().for_each(
4066        |(entity, maybe_children, cached_pipelines, mut cached_ready_state)| {
4067            // Update the ready state of this root effect
4068            let changed = cached_pipelines.is_changed() || cached_ready_state.is_added() || orphaned_entities.binary_search(&entity).is_ok();
4069            trace!("[Entity {}] changed={} cached_pipelines={} ready_state={}", entity, changed, cached_pipelines.is_ready(), cached_ready_state.is_ready);
4070            if changed {
4071                // Root effects by default are ready since they have no ancestors to check. After that we check the ready conditions for this effect alone.
4072                let new_ready_state = CachedReadyState::new(cached_pipelines.is_ready());
4073                if *cached_ready_state != new_ready_state {
4074                    debug!(
4075                        "[Entity {}] Changed ready to: {}",
4076                        entity,
4077                        new_ready_state.is_ready()
4078                    );
4079                    *cached_ready_state = new_ready_state;
4080                }
4081            }
4082
4083            // Recursively update the ready state of its descendants
4084            if let Some(children) = maybe_children {
4085                for (child, child_of) in q_child_effects.iter_many(children) {
4086                    assert_eq!(
4087                        child_of.parent, entity,
4088                        "Malformed hierarchy. This probably means that your hierarchy has been improperly maintained, or contains a cycle"
4089                    );
4090                    // SAFETY:
4091                    // - `child` must have consistent parentage, or the above assertion would panic.
4092                    //   Since `child` is parented to a root entity, the entire hierarchy leading to it
4093                    //   is consistent.
4094                    // - We may operate as if all descendants are consistent, since
4095                    //   `propagate_ready_state_recursive` will panic before continuing to propagate if it
4096                    //   encounters an entity with inconsistent parentage.
4097                    // - Since each root entity is unique and the hierarchy is consistent and
4098                    //   forest-like, other root entities' `propagate_ready_state_recursive` calls will not conflict
4099                    //   with this one.
4100                    // - Since this is the only place where `transform_query` gets used, there will be
4101                    //   no conflicting fetches elsewhere.
4102                    #[expect(unsafe_code, reason = "`propagate_ready_state_recursive()` is unsafe due to its use of `Query::get_unchecked()`.")]
4103                    unsafe {
4104                        propagate_ready_state_recursive(
4105                            &cached_ready_state,
4106                            &q_ready_state,
4107                            &q_child_effects,
4108                            child,
4109                            changed || child_of.is_changed(),
4110                        );
4111                    }
4112                }
4113            }
4114        },
4115    );
4116}
4117
4118#[expect(
4119    unsafe_code,
4120    reason = "This function uses `Query::get_unchecked()`, which can result in multiple mutable references if the preconditions are not met."
4121)]
4122unsafe fn propagate_ready_state_recursive(
4123    parent_state: &CachedReadyState,
4124    q_ready_state: &Query<
4125        (
4126            Ref<CachedPipelines>,
4127            &mut CachedReadyState,
4128            Option<&ChildrenEffects>,
4129        ),
4130        With<ChildEffectOf>,
4131    >,
4132    q_child_of: &Query<(Entity, Ref<ChildEffectOf>), With<CachedReadyState>>,
4133    entity: Entity,
4134    mut changed: bool,
4135) {
4136    // Update this effect in-place by checking its own state and the state of its
4137    // parent (which has already been propagated from all the parent's ancestors, so
4138    // is correct for this frame).
4139    let (cached_ready_state, maybe_children) = {
4140        let Ok((cached_pipelines, mut cached_ready_state, maybe_children)) =
4141        // SAFETY: Copied from Bevy's transform propagation, same reasoning
4142        (unsafe { q_ready_state.get_unchecked(entity) }) else {
4143            return;
4144        };
4145
4146        changed |= cached_pipelines.is_changed() || cached_ready_state.is_added();
4147        if changed {
4148            let new_ready_state =
4149                CachedReadyState::new(parent_state.is_ready()).and(cached_pipelines.is_ready());
4150            // Ensure we don't trigger ECS change detection here if state didn't change, so
4151            // we can avoid this effect branch on next iteration.
4152            if *cached_ready_state != new_ready_state {
4153                debug!(
4154                    "[Entity {}] Changed ready to: {}",
4155                    entity,
4156                    new_ready_state.is_ready()
4157                );
4158                *cached_ready_state = new_ready_state;
4159            }
4160        }
4161        (cached_ready_state, maybe_children)
4162    };
4163
4164    // Recurse into descendants
4165    let Some(children) = maybe_children else {
4166        return;
4167    };
4168    for (child, child_of) in q_child_of.iter_many(children) {
4169        assert_eq!(
4170        child_of.parent, entity,
4171        "Malformed hierarchy. This probably means that your hierarchy has been improperly maintained, or contains a cycle"
4172    );
4173        // SAFETY: The caller guarantees that `transform_query` will not be fetched for
4174        // any descendants of `entity`, so it is safe to call
4175        // `propagate_recursive` for each child.
4176        //
4177        // The above assertion ensures that each child has one and only one unique
4178        // parent throughout the entire hierarchy.
4179        unsafe {
4180            propagate_ready_state_recursive(
4181                cached_ready_state.as_ref(),
4182                q_ready_state,
4183                q_child_of,
4184                child,
4185                changed || child_of.is_changed(),
4186            );
4187        }
4188    }
4189}
4190
4191/// Once all effects are extracted and all cached components are updated, it's
4192/// time to prepare for sorting and batching. Collect all relevant data and
4193/// insert/update the [`BatchInput`] for each effect.
4194pub(crate) fn prepare_batch_inputs(
4195    mut commands: Commands,
4196    read_only_params: PrepareEffectsReadOnlyParams,
4197    pipeline_cache: Res<PipelineCache>,
4198    mut effects_meta: ResMut<EffectsMeta>,
4199    mut effect_bind_groups: ResMut<EffectBindGroups>,
4200    mut property_bind_groups: ResMut<PropertyBindGroups>,
4201    q_cached_effects: Query<(
4202        MainEntity,
4203        Entity,
4204        &ExtractedEffect,
4205        &ExtractedSpawner,
4206        &CachedEffect,
4207        &CachedEffectMetadata,
4208        &CachedReadyState,
4209        &CachedPipelines,
4210        Option<&CachedDrawIndirectArgs>,
4211        Option<&CachedParentInfo>,
4212        Option<&ChildEffectOf>,
4213        Option<&CachedChildInfo>,
4214        Option<&CachedEffectEvents>,
4215    )>,
4216    mut sort_bind_groups: ResMut<SortBindGroups>,
4217) {
4218    #[cfg(feature = "trace")]
4219    let _span = bevy::log::info_span!("prepare_batch_inputs").entered();
4220    trace!("prepare_batch_inputs");
4221
4222    // Workaround for too many params in system (TODO: refactor to split work?)
4223    let sim_params = read_only_params.sim_params.into_inner();
4224    let render_device = read_only_params.render_device.into_inner();
4225    let render_queue = read_only_params.render_queue.into_inner();
4226
4227    // Clear per-instance buffers, which are filled below and re-uploaded each frame
4228    effects_meta.spawner_buffer.clear();
4229
4230    // Build batcher inputs from extracted effects, updating all cached components
4231    // for each effect on the fly.
4232    let mut extracted_effect_count = 0;
4233    let mut prepared_effect_count = 0;
4234    for (
4235        main_entity,
4236        render_entity,
4237        extracted_effect,
4238        extracted_spawner,
4239        cached_effect,
4240        cached_effect_metadata,
4241        cached_ready_state,
4242        cached_pipelines,
4243        maybe_cached_draw_indirect_args,
4244        maybe_cached_parent_info,
4245        maybe_child_effect_of,
4246        maybe_cached_child_info,
4247        maybe_cached_effect_events,
4248    ) in &q_cached_effects
4249    {
4250        extracted_effect_count += 1;
4251
4252        // Skip this effect if not ready
4253        if !cached_ready_state.is_ready() {
4254            trace!("Pipelines not ready for effect {}, skipped.", render_entity);
4255            continue;
4256        }
4257
4258        // Skip this effect if not visible and not simulating when hidden
4259        if !extracted_spawner.is_visible
4260            && (extracted_effect.simulation_condition == SimulationCondition::WhenVisible)
4261        {
4262            trace!(
4263                "Effect {} not visible, and simulation condition is WhenVisible, so skipped.",
4264                render_entity
4265            );
4266            continue;
4267        }
4268
4269        // Fetch the init and update pipelines.
4270        // SAFETY: If is_ready() returns true, this means the pipelines are cached and
4271        // ready, so the IDs must be valid.
4272        let init_and_update_pipeline_ids = InitAndUpdatePipelineIds {
4273            init: cached_pipelines.init.unwrap(),
4274            update: cached_pipelines.update.unwrap(),
4275        };
4276
4277        let effect_slice = EffectSlice {
4278            slice: cached_effect.slice.range(),
4279            slab_id: cached_effect.slab_id,
4280            particle_layout: cached_effect.slice.particle_layout.clone(),
4281        };
4282
4283        // Fetch the bind group layouts from the cache
4284        trace!("child_effect_of={:?}", maybe_child_effect_of);
4285        let parent_slab_id = if let Some(child_effect_of) = maybe_child_effect_of {
4286            let Ok((_, _, _, _, parent_cached_effect, _, _, _, _, _, _, _, _)) =
4287                q_cached_effects.get(child_effect_of.parent)
4288            else {
4289                // At this point we should have discarded invalid effects with a missing parent,
4290                // so if the parent is not found this is a bug.
4291                error!(
4292                    "Effect main_entity {:?}: parent render entity {:?} not found.",
4293                    main_entity, child_effect_of.parent
4294                );
4295                continue;
4296            };
4297            Some(parent_cached_effect.slab_id)
4298        } else {
4299            None
4300        };
4301
4302        // For ribbons, we need the sorting pipeline to be ready to sort the ribbon's
4303        // particles by age in order to build a contiguous mesh.
4304        if extracted_effect.layout_flags.contains(LayoutFlags::RIBBONS) {
4305            // Ensure the bind group layout for sort-fill is ready. This will also ensure
4306            // the pipeline is created and queued if needed.
4307            if let Err(err) = sort_bind_groups.ensure_sort_fill_bind_group_layout_desc(
4308                &pipeline_cache,
4309                &extracted_effect.particle_layout,
4310            ) {
4311                error!(
4312                    "Failed to create bind group for ribbon effect sorting: {:?}",
4313                    err
4314                );
4315                continue;
4316            }
4317
4318            // Check sort pipelines are ready, otherwise we might desync some buffers if
4319            // running only some of them but not all.
4320            if !sort_bind_groups
4321                .is_pipeline_ready(&extracted_effect.particle_layout, &pipeline_cache)
4322            {
4323                trace!(
4324                    "Sort pipeline not ready for effect on main entity {:?}; skipped.",
4325                    main_entity
4326                );
4327                continue;
4328            }
4329        }
4330
4331        // Output some debug info
4332        trace!("init_shader = {:?}", extracted_effect.effect_shaders.init);
4333        trace!(
4334            "update_shader = {:?}",
4335            extracted_effect.effect_shaders.update
4336        );
4337        trace!(
4338            "render_shader = {:?}",
4339            extracted_effect.effect_shaders.render
4340        );
4341        trace!("layout_flags = {:?}", extracted_effect.layout_flags);
4342        trace!("particle_layout = {:?}", effect_slice.particle_layout);
4343
4344        let parent_slab_offset = maybe_cached_child_info.map(|cci| cci.parent_slab_offset);
4345
4346        assert!(cached_effect_metadata.table_id.is_valid());
4347        let spawner_index = effects_meta.allocate_spawner(
4348            &extracted_spawner.transform,
4349            extracted_spawner.spawn_count,
4350            extracted_spawner.prng_seed,
4351            cached_effect.slice.range().start,
4352            parent_slab_offset,
4353            cached_effect_metadata.table_id,
4354            maybe_cached_draw_indirect_args,
4355        );
4356
4357        trace!("Updating cached effect at entity {render_entity:?}...");
4358        let mut cmd = commands.entity(render_entity);
4359        // Inserting the BatchInput component marks the effect as ready for this frame
4360        cmd.insert(BatchInput {
4361            effect_slice,
4362            init_and_update_pipeline_ids,
4363            parent_slab_id,
4364            event_buffer_index: maybe_cached_effect_events.map(|cee| cee.buffer_index),
4365            child_effects: maybe_cached_parent_info
4366                .as_ref()
4367                .map(|cp| cp.children.clone())
4368                .unwrap_or_default(),
4369            spawner_index,
4370            init_indirect_dispatch_index: maybe_cached_child_info
4371                .as_ref()
4372                .map(|cc| cc.init_indirect_dispatch_index),
4373        });
4374
4375        prepared_effect_count += 1;
4376    }
4377    trace!("Prepared {prepared_effect_count}/{extracted_effect_count} extracted effect(s)");
4378
4379    // Update simulation parameters, including the total effect count for this frame
4380    {
4381        let mut gpu_sim_params: GpuSimParams = sim_params.into();
4382        gpu_sim_params.num_effects = prepared_effect_count;
4383        trace!(
4384            "Simulation parameters: time={} delta_time={} virtual_time={} \
4385                virtual_delta_time={} real_time={} real_delta_time={} num_effects={}",
4386            gpu_sim_params.time,
4387            gpu_sim_params.delta_time,
4388            gpu_sim_params.virtual_time,
4389            gpu_sim_params.virtual_delta_time,
4390            gpu_sim_params.real_time,
4391            gpu_sim_params.real_delta_time,
4392            gpu_sim_params.num_effects,
4393        );
4394        effects_meta.sim_params_uniforms.set(gpu_sim_params);
4395    }
4396
4397    // Write the entire spawner buffer for this frame, for all effects combined
4398    assert_eq!(
4399        prepared_effect_count,
4400        effects_meta.spawner_buffer.len() as u32
4401    );
4402    if effects_meta
4403        .spawner_buffer
4404        .write_buffer(render_device, render_queue)
4405    {
4406        // All property bind groups use the spawner buffer, which was reallocate
4407        effect_bind_groups.particle_slabs.clear();
4408        property_bind_groups.clear(true);
4409        effects_meta.indirect_spawner_bind_group = None;
4410    }
4411}
4412
4413/// Batch compatible effects together into a single pass.
4414///
4415/// For all effects marked as ready for this frame (have a BatchInput
4416/// component), sort the effects by grouping compatible effects together, then
4417/// batch those groups together. Each batch can be updated and rendered with a
4418/// single compute dispatch or draw call.
4419pub(crate) fn batch_effects(
4420    mut commands: Commands,
4421    effects_meta: Res<EffectsMeta>,
4422    mut sort_bind_groups: ResMut<SortBindGroups>,
4423    mut q_cached_effects: Query<(
4424        Entity,
4425        &MainEntity,
4426        &ExtractedEffect,
4427        &ExtractedSpawner,
4428        &ExtractedEffectMesh,
4429        &CachedDrawIndirectArgs,
4430        &CachedEffectMetadata,
4431        Option<&CachedEffectEvents>,
4432        Option<&ChildEffectOf>,
4433        Option<&CachedChildInfo>,
4434        Option<&CachedEffectProperties>,
4435        &mut DispatchBufferIndices,
4436        // The presence of BatchInput ensure the effect is ready
4437        &mut BatchInput,
4438    )>,
4439    mut sorted_effect_batches: ResMut<SortedEffectBatches>,
4440    mut gpu_buffer_operations: ResMut<GpuBufferOperations>,
4441) {
4442    #[cfg(feature = "trace")]
4443    let _span = bevy::log::info_span!("batch_effects").entered();
4444    trace!("batch_effects");
4445
4446    // Sort effects in batching order, so that we can batch by simply doing a linear
4447    // scan of the effects in this order. Currently compatible effects mean:
4448    // - same effect slab (so we can bind the buffers once for all batched effects)
4449    // - in order of increasing sub-allocation inside those buffers (to make the
4450    //   sort stable)
4451    // - with parents before their children, to ensure ???? FIXME don't we need to
4452    //   opposite?!!!
4453    let mut effect_sorter = EffectSorter::new();
4454    for (entity, _, _, _, _, _, _, _, child_of, _, _, _, input) in &q_cached_effects {
4455        effect_sorter.insert(
4456            entity,
4457            input.effect_slice.slab_id,
4458            input.effect_slice.slice.start,
4459            child_of.map(|co| co.parent),
4460        );
4461    }
4462    effect_sorter.sort();
4463
4464    // For now we re-create that buffer each frame. Since there's no CPU -> GPU
4465    // transfer, this is pretty cheap in practice.
4466    sort_bind_groups.clear_indirect_dispatch_buffer();
4467
4468    let mut sort_queue = GpuBufferOperationQueue::new();
4469
4470    // Loop on all extracted effects in sorted order, and try to batch them together
4471    // to reduce draw calls. -- currently does nothing, batching was broken and
4472    // never fixed, but at least we minimize the GPU state changes with the sorting!
4473    trace!("Batching {} effects...", q_cached_effects.iter().len());
4474    sorted_effect_batches.clear();
4475    for entity in effect_sorter.effects.iter().map(|e| e.entity) {
4476        let Ok((
4477            entity,
4478            main_entity,
4479            extracted_effect,
4480            extracted_spawner,
4481            extracted_effect_mesh,
4482            cached_draw_indirect_args,
4483            cached_effect_metadata,
4484            cached_effect_events,
4485            _,
4486            cached_child_info,
4487            cached_properties,
4488            dispatch_buffer_indices,
4489            mut input,
4490        )) = q_cached_effects.get_mut(entity)
4491        else {
4492            continue;
4493        };
4494
4495        let translation = extracted_spawner.transform.translation();
4496
4497        // Spawn one EffectBatch per instance (no batching; TODO). This contains
4498        // most of the data needed to drive rendering. However this doesn't drive
4499        // rendering; this is just storage.
4500        let mut effect_batch = EffectBatch::from_input(
4501            main_entity.id(),
4502            extracted_effect,
4503            extracted_spawner,
4504            extracted_effect_mesh,
4505            cached_effect_events,
4506            cached_child_info,
4507            &mut input,
4508            *dispatch_buffer_indices,
4509            cached_draw_indirect_args.row,
4510            cached_effect_metadata.table_id,
4511            cached_properties.map(|cp| PropertyBindGroupKey {
4512                buffer_index: cp.buffer_index,
4513                binding_size: cp.property_layout.min_binding_size().get() as u32,
4514            }),
4515            cached_properties.map(|cp| cp.range.start),
4516        );
4517
4518        // If the batch has ribbons, we need to sort the particles by RIBBON_ID and AGE
4519        // for ribbon meshing, in order to avoid gaps when some particles in the middle
4520        // of the ribbon die (since we can't guarantee a linear lifetime through the
4521        // ribbon).
4522        if extracted_effect.layout_flags.contains(LayoutFlags::RIBBONS) {
4523            // This buffer is allocated in prepare_effects(), so should always be available
4524            let Some(effect_metadata_buffer) = effects_meta.effect_metadata_buffer.buffer() else {
4525                error!("Failed to find effect metadata buffer. This is a bug.");
4526                continue;
4527            };
4528
4529            // Allocate a GpuDispatchIndirect entry
4530            let sort_fill_indirect_dispatch_index = sort_bind_groups.allocate_indirect_dispatch();
4531            effect_batch.sort_fill_indirect_dispatch_index =
4532                Some(sort_fill_indirect_dispatch_index);
4533
4534            // Enqueue a fill dispatch operation which reads GpuEffectMetadata::alive_count,
4535            // compute a number of workgroups to dispatch based on that particle count, and
4536            // store the result into a GpuDispatchIndirect struct which will be used to
4537            // dispatch the fill-sort pass.
4538            {
4539                let src_buffer = effect_metadata_buffer.clone();
4540                let src_binding_offset = effects_meta
4541                    .effect_metadata_buffer
4542                    .dynamic_offset(effect_batch.metadata_table_id);
4543                let src_binding_size = effects_meta.gpu_limits.effect_metadata_aligned_size;
4544                let Some(dst_buffer) = sort_bind_groups.indirect_buffer() else {
4545                    error!("Missing indirect dispatch buffer for sorting, cannot schedule particle sort for ribbon. This is a bug.");
4546                    continue;
4547                };
4548                let dst_buffer = dst_buffer.clone();
4549                let dst_binding_offset = 0; // see dst_offset below
4550                                            //let dst_binding_size = NonZeroU32::new(12).unwrap();
4551                trace!(
4552                    "queue_fill_dispatch(): src#{:?}@+{}B ({}B) -> dst#{:?}@+{}B ({}B)",
4553                    src_buffer.id(),
4554                    src_binding_offset,
4555                    src_binding_size.get(),
4556                    dst_buffer.id(),
4557                    dst_binding_offset,
4558                    -1, //dst_binding_size.get(),
4559                );
4560                let src_offset = std::mem::offset_of!(GpuEffectMetadata, alive_count) as u32 / 4;
4561                debug_assert_eq!(
4562                    src_offset, 1,
4563                    "GpuEffectMetadata changed, update this assert."
4564                );
4565                // FIXME - This is a quick fix to get 0.15 out. The previous code used the
4566                // dynamic binding offset, but the indirect dispatch structs are only 12 bytes,
4567                // so are not aligned to min_storage_buffer_offset_alignment. The fix uses a
4568                // binding offset of 0 and binds the entire destination buffer,
4569                // then use the dst_offset value embedded inside the GpuBufferOperationArgs to
4570                // index the proper offset in the buffer. This requires of
4571                // course binding the entire buffer, or at least enough to index all operations
4572                // (hence the None below). This is not really a general solution, so should be
4573                // reviewed.
4574                let dst_offset = sort_bind_groups
4575                    .get_indirect_dispatch_byte_offset(sort_fill_indirect_dispatch_index)
4576                    / 4;
4577                sort_queue.enqueue(
4578                    GpuBufferOperationType::FillDispatchArgs,
4579                    GpuBufferOperationArgs {
4580                        src_offset,
4581                        src_stride: effects_meta.gpu_limits.effect_metadata_aligned_size.get() / 4,
4582                        dst_offset,
4583                        dst_stride: GpuDispatchIndirectArgs::SHADER_SIZE.get() as u32 / 4,
4584                        count: 1,
4585                    },
4586                    src_buffer,
4587                    src_binding_offset,
4588                    Some(src_binding_size),
4589                    dst_buffer,
4590                    dst_binding_offset,
4591                    None, //Some(dst_binding_size),
4592                );
4593            }
4594        }
4595
4596        let effect_batch_index = sorted_effect_batches.push(effect_batch);
4597        trace!(
4598            "Spawned effect batch #{:?} from cached instance on entity {:?}.",
4599            effect_batch_index,
4600            entity,
4601        );
4602
4603        // Spawn an EffectDrawBatch, to actually drive rendering.
4604        commands
4605            .spawn(EffectDrawBatch {
4606                effect_batch_index,
4607                translation,
4608                main_entity: *main_entity,
4609            })
4610            .insert(TemporaryRenderEntity);
4611    }
4612
4613    gpu_buffer_operations.begin_frame();
4614    debug_assert!(sorted_effect_batches.dispatch_queue_index.is_none());
4615    if !sort_queue.operation_queue.is_empty() {
4616        sorted_effect_batches.dispatch_queue_index = Some(gpu_buffer_operations.submit(sort_queue));
4617    }
4618}
4619
4620/// Per-buffer bind groups for a GPU effect buffer.
4621///
4622/// This contains all bind groups specific to a single [`EffectBuffer`].
4623///
4624/// [`EffectBuffer`]: crate::render::effect_cache::EffectBuffer
4625pub(crate) struct BufferBindGroups {
4626    /// Bind group for the render shader.
4627    ///
4628    /// ```wgsl
4629    /// @binding(0) var<storage, read> particle_buffer : ParticleBuffer;
4630    /// @binding(1) var<storage, read> indirect_buffer : IndirectBuffer;
4631    /// @binding(2) var<storage, read> spawner : Spawner;
4632    /// ```
4633    render: BindGroup,
4634    // /// Bind group for filling the indirect dispatch arguments of any child init
4635    // /// pass.
4636    // ///
4637    // /// This bind group is optional; it's only created if the current effect has
4638    // /// a GPU spawn event buffer, irrelevant of whether it has child effects
4639    // /// (although normally the event buffer is not created if there's no
4640    // /// children).
4641    // ///
4642    // /// The source buffer is always the current effect's event buffer. The
4643    // /// destination buffer is the global shared buffer for indirect fill args
4644    // /// operations owned by the [`EffectCache`]. The uniform buffer of operation
4645    // /// args contains the data to index the relevant part of the global shared
4646    // /// buffer for this effect buffer; it may contain multiple entries in case
4647    // /// multiple effects are batched inside the current effect buffer.
4648    // ///
4649    // /// ```wgsl
4650    // /// @group(0) @binding(0) var<uniform> args : BufferOperationArgs;
4651    // /// @group(0) @binding(1) var<storage, read> src_buffer : array<u32>;
4652    // /// @group(0) @binding(2) var<storage, read_write> dst_buffer : array<u32>;
4653    // /// ```
4654    // init_fill_dispatch: Option<BindGroup>,
4655}
4656
4657/// Combination of a texture layout and the bound textures.
4658#[derive(Debug, Default, Clone, PartialEq, Eq, Hash)]
4659struct Material {
4660    layout: TextureLayout,
4661    textures: Vec<AssetId<Image>>,
4662}
4663
4664impl Material {
4665    /// Get the bind group entries to create a bind group.
4666    pub fn make_entries<'a>(
4667        &self,
4668        gpu_images: &'a RenderAssets<GpuImage>,
4669    ) -> Result<Vec<BindGroupEntry<'a>>, ()> {
4670        if self.textures.is_empty() {
4671            return Ok(vec![]);
4672        }
4673
4674        let entries: Vec<BindGroupEntry<'a>> = self
4675            .textures
4676            .iter()
4677            .enumerate()
4678            .flat_map(|(index, id)| {
4679                let base_binding = index as u32 * 2;
4680                if let Some(gpu_image) = gpu_images.get(*id) {
4681                    vec![
4682                        BindGroupEntry {
4683                            binding: base_binding,
4684                            resource: BindingResource::TextureView(&gpu_image.texture_view),
4685                        },
4686                        BindGroupEntry {
4687                            binding: base_binding + 1,
4688                            resource: BindingResource::Sampler(&gpu_image.sampler),
4689                        },
4690                    ]
4691                } else {
4692                    vec![]
4693                }
4694            })
4695            .collect();
4696        if entries.len() == self.textures.len() * 2 {
4697            return Ok(entries);
4698        }
4699        Err(())
4700    }
4701}
4702
4703#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
4704struct BindingKey {
4705    pub buffer_id: BufferId,
4706    pub offset: u32,
4707    pub size: NonZeroU32,
4708}
4709
4710impl<'a> From<BufferSlice<'a>> for BindingKey {
4711    fn from(value: BufferSlice<'a>) -> Self {
4712        Self {
4713            buffer_id: value.buffer.id(),
4714            offset: value.offset,
4715            size: value.size,
4716        }
4717    }
4718}
4719
4720impl<'a> From<&BufferSlice<'a>> for BindingKey {
4721    fn from(value: &BufferSlice<'a>) -> Self {
4722        Self {
4723            buffer_id: value.buffer.id(),
4724            offset: value.offset,
4725            size: value.size,
4726        }
4727    }
4728}
4729
4730impl From<&BufferBindingSource> for BindingKey {
4731    fn from(value: &BufferBindingSource) -> Self {
4732        Self {
4733            buffer_id: value.buffer.id(),
4734            offset: value.offset,
4735            size: value.size,
4736        }
4737    }
4738}
4739
4740#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
4741struct ConsumeEventKey {
4742    child_infos_buffer_id: BufferId,
4743    events: BindingKey,
4744}
4745
4746impl From<&ConsumeEventBuffers<'_>> for ConsumeEventKey {
4747    fn from(value: &ConsumeEventBuffers) -> Self {
4748        Self {
4749            child_infos_buffer_id: value.child_infos_buffer.id(),
4750            events: value.events.into(),
4751        }
4752    }
4753}
4754
4755#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
4756struct InitMetadataBindGroupKey {
4757    pub slab_id: SlabId,
4758    pub effect_metadata_buffer: BufferId,
4759    pub consume_event_key: Option<ConsumeEventKey>,
4760}
4761
4762#[derive(Debug, Clone, PartialEq, Eq, Hash)]
4763struct UpdateMetadataBindGroupKey {
4764    pub slab_id: SlabId,
4765    pub effect_metadata_buffer: BufferId,
4766    pub child_info_buffer_id: Option<BufferId>,
4767    pub event_buffers_keys: Vec<BindingKey>,
4768}
4769
4770/// Bind group cached with an associated key.
4771///
4772/// The cached bind group is associated with the given key representing the
4773/// inputs that the bind group depends on. When those inputs change, the key
4774/// should change, indicating the bind group needs to be recreated.
4775///
4776/// This object manages a single bind group and its key.
4777struct CachedBindGroup<K: Eq> {
4778    /// Key the bind group was created from. Each time the key changes, the bind
4779    /// group should be re-created.
4780    key: K,
4781    /// Bind group created from the key.
4782    bind_group: BindGroup,
4783}
4784
4785#[derive(Debug, Clone, Copy)]
4786struct BufferSlice<'a> {
4787    pub buffer: &'a Buffer,
4788    pub offset: u32,
4789    pub size: NonZeroU32,
4790}
4791
4792impl<'a> From<BufferSlice<'a>> for BufferBinding<'a> {
4793    fn from(value: BufferSlice<'a>) -> Self {
4794        Self {
4795            buffer: value.buffer,
4796            offset: value.offset.into(),
4797            size: Some(value.size.into()),
4798        }
4799    }
4800}
4801
4802impl<'a> From<&BufferSlice<'a>> for BufferBinding<'a> {
4803    fn from(value: &BufferSlice<'a>) -> Self {
4804        Self {
4805            buffer: value.buffer,
4806            offset: value.offset.into(),
4807            size: Some(value.size.into()),
4808        }
4809    }
4810}
4811
4812impl<'a> From<&'a BufferBindingSource> for BufferSlice<'a> {
4813    fn from(value: &'a BufferBindingSource) -> Self {
4814        Self {
4815            buffer: &value.buffer,
4816            offset: value.offset,
4817            size: value.size,
4818        }
4819    }
4820}
4821
4822/// Optional input to [`EffectBindGroups::get_or_create_init_metadata()`] when
4823/// the init pass consumes GPU events as a mechanism to spawn particles.
4824struct ConsumeEventBuffers<'a> {
4825    /// Entire buffer containing the [`GpuChildInfo`] entries for all effects.
4826    /// This is dynamically indexed inside the shader.
4827    child_infos_buffer: &'a Buffer,
4828    /// Slice of the [`EventBuffer`] where the GPU spawn events are stored.
4829    events: BufferSlice<'a>,
4830}
4831
4832#[derive(Default, Resource)]
4833pub struct EffectBindGroups {
4834    /// Map from a slab ID to the bind groups shared among all effects that
4835    /// use that particle slab.
4836    particle_slabs: HashMap<SlabId, BufferBindGroups>,
4837    /// Map of bind groups for image assets used as particle textures.
4838    images: HashMap<AssetId<Image>, BindGroup>,
4839    /// Map from buffer index to its metadata bind group (group 3) for the init
4840    /// pass.
4841    // FIXME - doesn't work with batching; this should be the instance ID
4842    init_metadata_bind_groups: HashMap<SlabId, CachedBindGroup<InitMetadataBindGroupKey>>,
4843    /// Map from buffer index to its metadata bind group (group 3) for the
4844    /// update pass.
4845    // FIXME - doesn't work with batching; this should be the instance ID
4846    update_metadata_bind_groups: HashMap<SlabId, CachedBindGroup<UpdateMetadataBindGroupKey>>,
4847    /// Map from an effect material to its bind group.
4848    material_bind_groups: HashMap<Material, BindGroup>,
4849}
4850
4851impl EffectBindGroups {
4852    pub fn particle_render(&self, slab_id: &SlabId) -> Option<&BindGroup> {
4853        self.particle_slabs.get(slab_id).map(|bg| &bg.render)
4854    }
4855
4856    /// Retrieve the metadata@3 bind group for the init pass, creating it if
4857    /// needed.
4858    pub(self) fn get_or_create_init_metadata(
4859        &mut self,
4860        effect_batch: &EffectBatch,
4861        render_device: &RenderDevice,
4862        layout: &BindGroupLayout,
4863        effect_metadata_buffer: &Buffer,
4864        consume_event_buffers: Option<ConsumeEventBuffers>,
4865    ) -> Result<&BindGroup, ()> {
4866        assert!(effect_batch.metadata_table_id.is_valid());
4867
4868        let key = InitMetadataBindGroupKey {
4869            slab_id: effect_batch.slab_id,
4870            effect_metadata_buffer: effect_metadata_buffer.id(),
4871            consume_event_key: consume_event_buffers.as_ref().map(Into::into),
4872        };
4873
4874        let make_entry = || {
4875            let mut entries = Vec::with_capacity(3);
4876            entries.push(
4877                // @group(3) @binding(0) var<storage, read_write> effect_metadatas :
4878                // array<EffectMetadata>;
4879                BindGroupEntry {
4880                    binding: 0,
4881                    resource: effect_metadata_buffer.as_entire_binding(),
4882                },
4883            );
4884            if let Some(consume_event_buffers) = consume_event_buffers.as_ref() {
4885                entries.push(
4886                    // @group(3) @binding(1) var<storage, read> child_info_buffer :
4887                    // ChildInfoBuffer;
4888                    BindGroupEntry {
4889                        binding: 1,
4890                        resource: BindingResource::Buffer(BufferBinding {
4891                            buffer: consume_event_buffers.child_infos_buffer,
4892                            offset: 0,
4893                            size: None,
4894                        }),
4895                    },
4896                );
4897                entries.push(
4898                    // @group(3) @binding(2) var<storage, read> event_buffer : EventBuffer;
4899                    BindGroupEntry {
4900                        binding: 2,
4901                        resource: BindingResource::Buffer(consume_event_buffers.events.into()),
4902                    },
4903                );
4904            }
4905
4906            let bind_group = render_device.create_bind_group(
4907                "hanabi:bind_group:init:metadata@3",
4908                layout,
4909                &entries[..],
4910            );
4911
4912            trace!(
4913                    "Created new metadata@3 bind group for init pass and buffer index {}: effect_metadata=#{}",
4914                    effect_batch.slab_id.index(),
4915                    effect_batch.metadata_table_id.0,
4916                );
4917
4918            bind_group
4919        };
4920
4921        Ok(&self
4922            .init_metadata_bind_groups
4923            .entry(effect_batch.slab_id)
4924            .and_modify(|cbg| {
4925                if cbg.key != key {
4926                    trace!(
4927                        "Bind group key changed for init metadata@3, re-creating bind group... old={:?} new={:?}",
4928                        cbg.key,
4929                        key
4930                    );
4931                    cbg.key = key;
4932                    cbg.bind_group = make_entry();
4933                }
4934            })
4935            .or_insert_with(|| {
4936                trace!("Inserting new bind group for init metadata@3 with key={:?}", key);
4937                CachedBindGroup {
4938                    key,
4939                    bind_group: make_entry(),
4940                }
4941            })
4942            .bind_group)
4943    }
4944
4945    /// Retrieve the metadata@3 bind group for the update pass, creating it if
4946    /// needed.
4947    pub(self) fn get_or_create_update_metadata(
4948        &mut self,
4949        effect_batch: &EffectBatch,
4950        render_device: &RenderDevice,
4951        layout: &BindGroupLayout,
4952        effect_metadata_buffer: &Buffer,
4953        child_info_buffer: Option<&Buffer>,
4954        event_buffers: &[(Entity, BufferBindingSource)],
4955    ) -> Result<&BindGroup, ()> {
4956        assert!(effect_batch.metadata_table_id.is_valid());
4957
4958        // Check arguments consistency
4959        assert_eq!(effect_batch.child_event_buffers.len(), event_buffers.len());
4960        let emits_gpu_spawn_events = !event_buffers.is_empty();
4961        let child_info_buffer_id = if emits_gpu_spawn_events {
4962            child_info_buffer.as_ref().map(|buffer| buffer.id())
4963        } else {
4964            // Note: child_info_buffer can be Some() if allocated, but we only consider it
4965            // if relevant, that is if the effect emits GPU spawn events.
4966            None
4967        };
4968        assert_eq!(emits_gpu_spawn_events, child_info_buffer_id.is_some());
4969
4970        let event_buffers_keys = event_buffers
4971            .iter()
4972            .map(|(_, buffer_binding_source)| buffer_binding_source.into())
4973            .collect::<Vec<_>>();
4974
4975        let key = UpdateMetadataBindGroupKey {
4976            slab_id: effect_batch.slab_id,
4977            effect_metadata_buffer: effect_metadata_buffer.id(),
4978            child_info_buffer_id,
4979            event_buffers_keys,
4980        };
4981
4982        let make_entry = || {
4983            let mut entries = Vec::with_capacity(2 + event_buffers.len());
4984            // @group(3) @binding(0) var<storage, read_write> effect_metadatas :
4985            // array<EffectMetadata>;
4986            entries.push(BindGroupEntry {
4987                binding: 0,
4988                resource: effect_metadata_buffer.as_entire_binding(),
4989            });
4990            if emits_gpu_spawn_events {
4991                let child_info_buffer = child_info_buffer.unwrap();
4992
4993                // @group(3) @binding(1) var<storage, read_write> child_info_buffer :
4994                // ChildInfoBuffer;
4995                entries.push(BindGroupEntry {
4996                    binding: 1,
4997                    resource: BindingResource::Buffer(BufferBinding {
4998                        buffer: child_info_buffer,
4999                        offset: 0,
5000                        size: None,
5001                    }),
5002                });
5003
5004                for (index, (_, buffer_binding_source)) in event_buffers.iter().enumerate() {
5005                    // @group(3) @binding(2+N) var<storage, read_write> event_buffer_N :
5006                    // EventBuffer;
5007                    // FIXME - BufferBindingSource originally was for Events, counting in u32, but
5008                    // then moved to counting in bytes, so now need some conversion. Need to review
5009                    // all of this...
5010                    let mut buffer_binding: BufferBinding = buffer_binding_source.into();
5011                    buffer_binding.offset *= 4;
5012                    buffer_binding.size = buffer_binding
5013                        .size
5014                        .map(|sz| NonZeroU64::new(sz.get() * 4).unwrap());
5015                    entries.push(BindGroupEntry {
5016                        binding: 2 + index as u32,
5017                        resource: BindingResource::Buffer(buffer_binding),
5018                    });
5019                }
5020            }
5021
5022            let bind_group = render_device.create_bind_group(
5023                "hanabi:bind_group:update:metadata@3",
5024                layout,
5025                &entries[..],
5026            );
5027
5028            trace!(
5029                "Created new metadata@3 bind group for update pass and slab ID {}: effect_metadata={}",
5030                effect_batch.slab_id.index(),
5031                effect_batch.metadata_table_id.0,
5032            );
5033
5034            bind_group
5035        };
5036
5037        Ok(&self
5038            .update_metadata_bind_groups
5039            .entry(effect_batch.slab_id)
5040            .and_modify(|cbg| {
5041                if cbg.key != key {
5042                    trace!(
5043                        "Bind group key changed for update metadata@3, re-creating bind group... old={:?} new={:?}",
5044                        cbg.key,
5045                        key
5046                    );
5047                    cbg.key = key.clone();
5048                    cbg.bind_group = make_entry();
5049                }
5050            })
5051            .or_insert_with(|| {
5052                trace!(
5053                    "Inserting new bind group for update metadata@3 with key={:?}",
5054                    key
5055                );
5056                CachedBindGroup {
5057                    key: key.clone(),
5058                    bind_group: make_entry(),
5059                }
5060            })
5061            .bind_group)
5062    }
5063}
5064
5065#[derive(SystemParam)]
5066pub struct QueueEffectsReadOnlyParams<'w, 's> {
5067    #[cfg(feature = "2d")]
5068    draw_functions_2d: Res<'w, DrawFunctions<Transparent2d>>,
5069    #[cfg(feature = "3d")]
5070    draw_functions_3d: Res<'w, DrawFunctions<Transparent3d>>,
5071    #[cfg(feature = "3d")]
5072    draw_functions_alpha_mask: Res<'w, DrawFunctions<AlphaMask3d>>,
5073    #[cfg(feature = "3d")]
5074    draw_functions_opaque: Res<'w, DrawFunctions<Opaque3d>>,
5075    marker: PhantomData<&'s usize>,
5076}
5077
5078fn emit_sorted_draw<T, F>(
5079    views: &Query<(&RenderVisibleEntities, &ExtractedView, &Msaa)>,
5080    render_phases: &mut ResMut<ViewSortedRenderPhases<T>>,
5081    view_entities: &mut FixedBitSet,
5082    sorted_effect_batches: &SortedEffectBatches,
5083    effect_draw_batches: &Query<(Entity, &mut EffectDrawBatch)>,
5084    render_pipeline: &mut ParticlesRenderPipeline,
5085    mut specialized_render_pipelines: Mut<SpecializedRenderPipelines<ParticlesRenderPipeline>>,
5086    render_meshes: &RenderAssets<RenderMesh>,
5087    pipeline_cache: &PipelineCache,
5088    make_phase_item: F,
5089    #[cfg(all(feature = "2d", feature = "3d"))] pipeline_mode: PipelineMode,
5090) where
5091    T: SortedPhaseItem,
5092    F: Fn(CachedRenderPipelineId, (Entity, MainEntity), &EffectDrawBatch, &ExtractedView) -> T,
5093{
5094    trace!("emit_sorted_draw() {} views", views.iter().len());
5095
5096    for (visible_entities, view, msaa) in views.iter() {
5097        trace!(
5098            "Process new sorted view with {} visible particle effect entities",
5099            visible_entities.len::<CompiledParticleEffect>()
5100        );
5101
5102        let Some(render_phase) = render_phases.get_mut(&view.retained_view_entity) else {
5103            continue;
5104        };
5105
5106        {
5107            #[cfg(feature = "trace")]
5108            let _span = bevy::log::info_span!("collect_view_entities").entered();
5109
5110            view_entities.clear();
5111            view_entities.extend(
5112                visible_entities
5113                    .iter::<EffectVisibilityClass>()
5114                    .map(|e| e.1.index_u32() as usize),
5115            );
5116        }
5117
5118        // For each view, loop over all the effect batches to determine if the effect
5119        // needs to be rendered for that view, and enqueue a view-dependent
5120        // batch if so.
5121        for (draw_entity, draw_batch) in effect_draw_batches.iter() {
5122            #[cfg(feature = "trace")]
5123            let _span_draw = bevy::log::info_span!("draw_batch").entered();
5124
5125            trace!(
5126                "Process draw batch: draw_entity={:?} effect_batch_index={:?}",
5127                draw_entity,
5128                draw_batch.effect_batch_index,
5129            );
5130
5131            // Get the EffectBatches this EffectDrawBatch is part of.
5132            let Some(effect_batch) = sorted_effect_batches.get(draw_batch.effect_batch_index)
5133            else {
5134                continue;
5135            };
5136
5137            trace!(
5138                "-> EffectBach: slab_id={} spawner_base={} layout_flags={:?}",
5139                effect_batch.slab_id.index(),
5140                effect_batch.spawner_base,
5141                effect_batch.layout_flags,
5142            );
5143
5144            // AlphaMask is a binned draw, so no sorted draw can possibly use it
5145            if effect_batch
5146                .layout_flags
5147                .intersects(LayoutFlags::USE_ALPHA_MASK | LayoutFlags::OPAQUE)
5148            {
5149                trace!("Non-transparent batch. Skipped.");
5150                continue;
5151            }
5152
5153            // Check if batch contains any entity visible in the current view. Otherwise we
5154            // can skip the entire batch. Note: This is O(n^2) but (unlike
5155            // the Sprite renderer this is inspired from) we don't expect more than
5156            // a handful of particle effect instances, so would rather not pay the memory
5157            // cost of a FixedBitSet for the sake of an arguable speed-up.
5158            // TODO - Profile to confirm.
5159            #[cfg(feature = "trace")]
5160            let _span_check_vis = bevy::log::info_span!("check_visibility").entered();
5161            let has_visible_entity = effect_batch
5162                .entities
5163                .iter()
5164                .any(|index| view_entities.contains(*index as usize));
5165            if !has_visible_entity {
5166                trace!("No visible entity for view, not emitting any draw call.");
5167                continue;
5168            }
5169            #[cfg(feature = "trace")]
5170            _span_check_vis.exit();
5171
5172            // Create and cache the bind group layout for this texture layout
5173            render_pipeline.cache_material(&effect_batch.texture_layout);
5174
5175            // FIXME - We draw the entire batch, but part of it may not be visible in this
5176            // view! We should re-batch for the current view specifically!
5177
5178            let local_space_simulation = effect_batch
5179                .layout_flags
5180                .contains(LayoutFlags::LOCAL_SPACE_SIMULATION);
5181            let alpha_mask = ParticleRenderAlphaMaskPipelineKey::from(effect_batch.layout_flags);
5182            let flipbook = effect_batch.layout_flags.contains(LayoutFlags::FLIPBOOK);
5183            let needs_uv = effect_batch.layout_flags.contains(LayoutFlags::NEEDS_UV);
5184            let needs_normal = effect_batch
5185                .layout_flags
5186                .contains(LayoutFlags::NEEDS_NORMAL);
5187            let needs_particle_fragment = effect_batch
5188                .layout_flags
5189                .contains(LayoutFlags::NEEDS_PARTICLE_FRAGMENT);
5190            let ribbons = effect_batch.layout_flags.contains(LayoutFlags::RIBBONS);
5191            let image_count = effect_batch.texture_layout.layout.len() as u8;
5192
5193            // FIXME - Maybe it's better to copy the mesh layout into the batch, instead of
5194            // re-querying here...?
5195            let Some(render_mesh) = render_meshes.get(effect_batch.mesh) else {
5196                trace!("Batch has no render mesh, skipped.");
5197                continue;
5198            };
5199            let mesh_layout = render_mesh.layout.clone();
5200
5201            // Specialize the render pipeline based on the effect batch
5202            trace!(
5203                "Specializing render pipeline: render_shader={:?} image_count={} alpha_mask={:?} flipbook={:?} hdr={}",
5204                effect_batch.render_shader,
5205                image_count,
5206                alpha_mask,
5207                flipbook,
5208                view.hdr
5209            );
5210
5211            // Add a draw pass for the effect batch
5212            trace!("Emitting individual draw for batch");
5213
5214            let alpha_mode = effect_batch.alpha_mode;
5215
5216            #[cfg(feature = "trace")]
5217            let _span_specialize = bevy::log::info_span!("specialize").entered();
5218            let render_pipeline_id = specialized_render_pipelines.specialize(
5219                pipeline_cache,
5220                render_pipeline,
5221                ParticleRenderPipelineKey {
5222                    shader: effect_batch.render_shader.clone(),
5223                    mesh_layout: Some(mesh_layout),
5224                    particle_layout: effect_batch.particle_layout.clone(),
5225                    texture_layout: effect_batch.texture_layout.clone(),
5226                    local_space_simulation,
5227                    alpha_mask,
5228                    alpha_mode,
5229                    flipbook,
5230                    needs_uv,
5231                    needs_normal,
5232                    needs_particle_fragment,
5233                    ribbons,
5234                    #[cfg(all(feature = "2d", feature = "3d"))]
5235                    pipeline_mode,
5236                    msaa_samples: msaa.samples(),
5237                    hdr: view.hdr,
5238                },
5239            );
5240            #[cfg(feature = "trace")]
5241            _span_specialize.exit();
5242
5243            trace!("+ Render pipeline specialized: id={:?}", render_pipeline_id,);
5244            trace!(
5245                "+ Add Transparent for batch on draw_entity {:?}: slab_id={} \
5246                spawner_base={} handle={:?}",
5247                draw_entity,
5248                effect_batch.slab_id.index(),
5249                effect_batch.spawner_base,
5250                effect_batch.handle
5251            );
5252            render_phase.add(make_phase_item(
5253                render_pipeline_id,
5254                (draw_entity, MainEntity::from(Entity::PLACEHOLDER)),
5255                draw_batch,
5256                view,
5257            ));
5258        }
5259    }
5260}
5261
5262#[cfg(feature = "3d")]
5263fn emit_binned_draw<T, F, G>(
5264    views: &Query<(&RenderVisibleEntities, &ExtractedView, &Msaa)>,
5265    render_phases: &mut ResMut<ViewBinnedRenderPhases<T>>,
5266    view_entities: &mut FixedBitSet,
5267    sorted_effect_batches: &SortedEffectBatches,
5268    effect_draw_batches: &Query<(Entity, &mut EffectDrawBatch)>,
5269    render_pipeline: &mut ParticlesRenderPipeline,
5270    mut specialized_render_pipelines: Mut<SpecializedRenderPipelines<ParticlesRenderPipeline>>,
5271    pipeline_cache: &PipelineCache,
5272    render_meshes: &RenderAssets<RenderMesh>,
5273    make_batch_set_key: F,
5274    make_bin_key: G,
5275    #[cfg(all(feature = "2d", feature = "3d"))] pipeline_mode: PipelineMode,
5276    alpha_mask: ParticleRenderAlphaMaskPipelineKey,
5277    change_tick: &mut Tick,
5278) where
5279    T: BinnedPhaseItem,
5280    F: Fn(CachedRenderPipelineId, &EffectDrawBatch, &ExtractedView) -> T::BatchSetKey,
5281    G: Fn() -> T::BinKey,
5282{
5283    use bevy::render::render_phase::{BinnedRenderPhaseType, InputUniformIndex};
5284
5285    trace!("emit_binned_draw() {} views", views.iter().len());
5286
5287    for (visible_entities, view, msaa) in views.iter() {
5288        trace!("Process new binned view (alpha_mask={:?})", alpha_mask);
5289
5290        let Some(render_phase) = render_phases.get_mut(&view.retained_view_entity) else {
5291            continue;
5292        };
5293
5294        {
5295            #[cfg(feature = "trace")]
5296            let _span = bevy::log::info_span!("collect_view_entities").entered();
5297
5298            view_entities.clear();
5299            view_entities.extend(
5300                visible_entities
5301                    .iter::<EffectVisibilityClass>()
5302                    .map(|e| e.1.index_u32() as usize),
5303            );
5304        }
5305
5306        // For each view, loop over all the effect batches to determine if the effect
5307        // needs to be rendered for that view, and enqueue a view-dependent
5308        // batch if so.
5309        for (draw_entity, draw_batch) in effect_draw_batches.iter() {
5310            #[cfg(feature = "trace")]
5311            let _span_draw = bevy::log::info_span!("draw_batch").entered();
5312
5313            trace!(
5314                "Process draw batch: draw_entity={:?} effect_batch_index={:?}",
5315                draw_entity,
5316                draw_batch.effect_batch_index,
5317            );
5318
5319            // Get the EffectBatches this EffectDrawBatch is part of.
5320            let Some(effect_batch) = sorted_effect_batches.get(draw_batch.effect_batch_index)
5321            else {
5322                continue;
5323            };
5324
5325            trace!(
5326                "-> EffectBaches: slab_id={} spawner_base={} layout_flags={:?}",
5327                effect_batch.slab_id.index(),
5328                effect_batch.spawner_base,
5329                effect_batch.layout_flags,
5330            );
5331
5332            if ParticleRenderAlphaMaskPipelineKey::from(effect_batch.layout_flags) != alpha_mask {
5333                trace!(
5334                    "Mismatching alpha mask pipeline key (batches={:?}, expected={:?}). Skipped.",
5335                    effect_batch.layout_flags,
5336                    alpha_mask
5337                );
5338                continue;
5339            }
5340
5341            // Check if batch contains any entity visible in the current view. Otherwise we
5342            // can skip the entire batch. Note: This is O(n^2) but (unlike
5343            // the Sprite renderer this is inspired from) we don't expect more than
5344            // a handful of particle effect instances, so would rather not pay the memory
5345            // cost of a FixedBitSet for the sake of an arguable speed-up.
5346            // TODO - Profile to confirm.
5347            #[cfg(feature = "trace")]
5348            let _span_check_vis = bevy::log::info_span!("check_visibility").entered();
5349            let has_visible_entity = effect_batch
5350                .entities
5351                .iter()
5352                .any(|index| view_entities.contains(*index as usize));
5353            if !has_visible_entity {
5354                trace!("No visible entity for view, not emitting any draw call.");
5355                continue;
5356            }
5357            #[cfg(feature = "trace")]
5358            _span_check_vis.exit();
5359
5360            // Create and cache the bind group layout for this texture layout
5361            render_pipeline.cache_material(&effect_batch.texture_layout);
5362
5363            // FIXME - We draw the entire batch, but part of it may not be visible in this
5364            // view! We should re-batch for the current view specifically!
5365
5366            let local_space_simulation = effect_batch
5367                .layout_flags
5368                .contains(LayoutFlags::LOCAL_SPACE_SIMULATION);
5369            let alpha_mask = ParticleRenderAlphaMaskPipelineKey::from(effect_batch.layout_flags);
5370            let flipbook = effect_batch.layout_flags.contains(LayoutFlags::FLIPBOOK);
5371            let needs_uv = effect_batch.layout_flags.contains(LayoutFlags::NEEDS_UV);
5372            let needs_normal = effect_batch
5373                .layout_flags
5374                .contains(LayoutFlags::NEEDS_NORMAL);
5375            let needs_particle_fragment = effect_batch
5376                .layout_flags
5377                .contains(LayoutFlags::NEEDS_PARTICLE_FRAGMENT);
5378            let ribbons = effect_batch.layout_flags.contains(LayoutFlags::RIBBONS);
5379            let image_count = effect_batch.texture_layout.layout.len() as u8;
5380            let render_mesh = render_meshes.get(effect_batch.mesh);
5381
5382            // Specialize the render pipeline based on the effect batch
5383            trace!(
5384                "Specializing render pipeline: render_shaders={:?} image_count={} alpha_mask={:?} flipbook={:?} hdr={}",
5385                effect_batch.render_shader,
5386                image_count,
5387                alpha_mask,
5388                flipbook,
5389                view.hdr
5390            );
5391
5392            // Add a draw pass for the effect batch
5393            trace!("Emitting individual draw for batch");
5394
5395            let alpha_mode = effect_batch.alpha_mode;
5396
5397            let Some(mesh_layout) = render_mesh.map(|gpu_mesh| gpu_mesh.layout.clone()) else {
5398                trace!("Missing mesh vertex buffer layout. Skipped.");
5399                continue;
5400            };
5401
5402            #[cfg(feature = "trace")]
5403            let _span_specialize = bevy::log::info_span!("specialize").entered();
5404            let render_pipeline_id = specialized_render_pipelines.specialize(
5405                pipeline_cache,
5406                render_pipeline,
5407                ParticleRenderPipelineKey {
5408                    shader: effect_batch.render_shader.clone(),
5409                    mesh_layout: Some(mesh_layout),
5410                    particle_layout: effect_batch.particle_layout.clone(),
5411                    texture_layout: effect_batch.texture_layout.clone(),
5412                    local_space_simulation,
5413                    alpha_mask,
5414                    alpha_mode,
5415                    flipbook,
5416                    needs_uv,
5417                    needs_normal,
5418                    needs_particle_fragment,
5419                    ribbons,
5420                    #[cfg(all(feature = "2d", feature = "3d"))]
5421                    pipeline_mode,
5422                    msaa_samples: msaa.samples(),
5423                    hdr: view.hdr,
5424                },
5425            );
5426            #[cfg(feature = "trace")]
5427            _span_specialize.exit();
5428
5429            trace!("+ Render pipeline specialized: id={:?}", render_pipeline_id,);
5430            trace!(
5431                "+ Add Transparent for batch on draw_entity {:?}: slab_id={} \
5432                spawner_base={} handle={:?}",
5433                draw_entity,
5434                effect_batch.slab_id.index(),
5435                effect_batch.spawner_base,
5436                effect_batch.handle
5437            );
5438            render_phase.add(
5439                make_batch_set_key(render_pipeline_id, draw_batch, view),
5440                make_bin_key(),
5441                (draw_entity, draw_batch.main_entity),
5442                InputUniformIndex::default(),
5443                BinnedRenderPhaseType::NonMesh,
5444                *change_tick,
5445            );
5446        }
5447    }
5448}
5449
5450#[allow(clippy::too_many_arguments)]
5451pub(crate) fn queue_effects(
5452    views: Query<(&RenderVisibleEntities, &ExtractedView, &Msaa)>,
5453    effects_meta: Res<EffectsMeta>,
5454    mut render_pipeline: ResMut<ParticlesRenderPipeline>,
5455    mut specialized_render_pipelines: ResMut<SpecializedRenderPipelines<ParticlesRenderPipeline>>,
5456    pipeline_cache: Res<PipelineCache>,
5457    mut effect_bind_groups: ResMut<EffectBindGroups>,
5458    sorted_effect_batches: Res<SortedEffectBatches>,
5459    effect_draw_batches: Query<(Entity, &mut EffectDrawBatch)>,
5460    events: Res<EffectAssetEvents>,
5461    render_meshes: Res<RenderAssets<RenderMesh>>,
5462    read_params: QueueEffectsReadOnlyParams,
5463    mut view_entities: Local<FixedBitSet>,
5464    #[cfg(feature = "2d")] mut transparent_2d_render_phases: ResMut<
5465        ViewSortedRenderPhases<Transparent2d>,
5466    >,
5467    #[cfg(feature = "3d")] mut transparent_3d_render_phases: ResMut<
5468        ViewSortedRenderPhases<Transparent3d>,
5469    >,
5470    #[cfg(feature = "3d")] (mut opaque_3d_render_phases, mut alpha_mask_3d_render_phases): (
5471        ResMut<ViewBinnedRenderPhases<Opaque3d>>,
5472        ResMut<ViewBinnedRenderPhases<AlphaMask3d>>,
5473    ),
5474    mut change_tick: Local<Tick>,
5475) {
5476    #[cfg(feature = "trace")]
5477    let _span = bevy::log::info_span!("hanabi:queue_effects").entered();
5478
5479    trace!("queue_effects");
5480
5481    // Bump the change tick so that Bevy is forced to rebuild the binned render
5482    // phase bins. We don't use the built-in caching so we don't want Bevy to
5483    // reuse stale data.
5484    let next_change_tick = change_tick.get() + 1;
5485    change_tick.set(next_change_tick);
5486
5487    // If an image has changed, the GpuImage has (probably) changed
5488    for event in &events.images {
5489        match event {
5490            AssetEvent::Added { .. } => (),
5491            AssetEvent::LoadedWithDependencies { .. } => (),
5492            AssetEvent::Unused { .. } => (),
5493            AssetEvent::Modified { id } => {
5494                if effect_bind_groups.images.remove(id).is_some() {
5495                    trace!("Destroyed bind group of modified image asset {:?}", id);
5496                }
5497            }
5498            AssetEvent::Removed { id } => {
5499                if effect_bind_groups.images.remove(id).is_some() {
5500                    trace!("Destroyes bind group of removed image asset {:?}", id);
5501                }
5502            }
5503        };
5504    }
5505
5506    if effects_meta.spawner_buffer.buffer().is_none() || effects_meta.spawner_buffer.is_empty() {
5507        // No spawners are active
5508        return;
5509    }
5510
5511    // Loop over all 2D cameras/views that need to render effects
5512    #[cfg(feature = "2d")]
5513    {
5514        #[cfg(feature = "trace")]
5515        let _span_draw = bevy::log::info_span!("draw_2d").entered();
5516
5517        let draw_effects_function_2d = read_params
5518            .draw_functions_2d
5519            .read()
5520            .get_id::<DrawEffects>()
5521            .unwrap();
5522
5523        // Effects with full alpha blending
5524        if !views.is_empty() {
5525            trace!("Emit effect draw calls for alpha blended 2D views...");
5526            emit_sorted_draw(
5527                &views,
5528                &mut transparent_2d_render_phases,
5529                &mut view_entities,
5530                &sorted_effect_batches,
5531                &effect_draw_batches,
5532                &mut render_pipeline,
5533                specialized_render_pipelines.reborrow(),
5534                &render_meshes,
5535                &pipeline_cache,
5536                |id, entity, draw_batch, _view| Transparent2d {
5537                    sort_key: FloatOrd(draw_batch.translation.z),
5538                    entity,
5539                    pipeline: id,
5540                    draw_function: draw_effects_function_2d,
5541                    batch_range: 0..1,
5542                    extracted_index: 0, // ???
5543                    extra_index: PhaseItemExtraIndex::None,
5544                    indexed: true, // ???
5545                },
5546                #[cfg(feature = "3d")]
5547                PipelineMode::Camera2d,
5548            );
5549        }
5550    }
5551
5552    // Loop over all 3D cameras/views that need to render effects
5553    #[cfg(feature = "3d")]
5554    {
5555        #[cfg(feature = "trace")]
5556        let _span_draw = bevy::log::info_span!("draw_3d").entered();
5557
5558        // Effects with full alpha blending
5559        if !views.is_empty() {
5560            trace!("Emit effect draw calls for alpha blended 3D views...");
5561
5562            let draw_effects_function_3d = read_params
5563                .draw_functions_3d
5564                .read()
5565                .get_id::<DrawEffects>()
5566                .unwrap();
5567
5568            emit_sorted_draw(
5569                &views,
5570                &mut transparent_3d_render_phases,
5571                &mut view_entities,
5572                &sorted_effect_batches,
5573                &effect_draw_batches,
5574                &mut render_pipeline,
5575                specialized_render_pipelines.reborrow(),
5576                &render_meshes,
5577                &pipeline_cache,
5578                |id, entity, batch, view| Transparent3d {
5579                    distance: view.rangefinder3d().distance(&batch.translation),
5580                    pipeline: id,
5581                    entity,
5582                    draw_function: draw_effects_function_3d,
5583                    batch_range: 0..1,
5584                    extra_index: PhaseItemExtraIndex::None,
5585                    indexed: true, // ???
5586                },
5587                #[cfg(feature = "2d")]
5588                PipelineMode::Camera3d,
5589            );
5590        }
5591
5592        // Effects with alpha mask
5593        if !views.is_empty() {
5594            #[cfg(feature = "trace")]
5595            let _span_draw = bevy::log::info_span!("draw_alphamask").entered();
5596
5597            trace!("Emit effect draw calls for alpha masked 3D views...");
5598
5599            let draw_effects_function_alpha_mask = read_params
5600                .draw_functions_alpha_mask
5601                .read()
5602                .get_id::<DrawEffects>()
5603                .unwrap();
5604
5605            emit_binned_draw(
5606                &views,
5607                &mut alpha_mask_3d_render_phases,
5608                &mut view_entities,
5609                &sorted_effect_batches,
5610                &effect_draw_batches,
5611                &mut render_pipeline,
5612                specialized_render_pipelines.reborrow(),
5613                &pipeline_cache,
5614                &render_meshes,
5615                |id, _batch, _view| OpaqueNoLightmap3dBatchSetKey {
5616                    pipeline: id,
5617                    draw_function: draw_effects_function_alpha_mask,
5618                    material_bind_group_index: None,
5619                    vertex_slab: default(),
5620                    index_slab: None,
5621                },
5622                // Unused for now
5623                || OpaqueNoLightmap3dBinKey {
5624                    asset_id: AssetId::<Mesh>::invalid().untyped(),
5625                },
5626                #[cfg(feature = "2d")]
5627                PipelineMode::Camera3d,
5628                ParticleRenderAlphaMaskPipelineKey::AlphaMask,
5629                &mut change_tick,
5630            );
5631        }
5632
5633        // Opaque particles
5634        if !views.is_empty() {
5635            #[cfg(feature = "trace")]
5636            let _span_draw = bevy::log::info_span!("draw_opaque").entered();
5637
5638            trace!("Emit effect draw calls for opaque 3D views...");
5639
5640            let draw_effects_function_opaque = read_params
5641                .draw_functions_opaque
5642                .read()
5643                .get_id::<DrawEffects>()
5644                .unwrap();
5645
5646            emit_binned_draw(
5647                &views,
5648                &mut opaque_3d_render_phases,
5649                &mut view_entities,
5650                &sorted_effect_batches,
5651                &effect_draw_batches,
5652                &mut render_pipeline,
5653                specialized_render_pipelines.reborrow(),
5654                &pipeline_cache,
5655                &render_meshes,
5656                |id, _batch, _view| Opaque3dBatchSetKey {
5657                    pipeline: id,
5658                    draw_function: draw_effects_function_opaque,
5659                    material_bind_group_index: None,
5660                    vertex_slab: default(),
5661                    index_slab: None,
5662                    lightmap_slab: None,
5663                },
5664                // Unused for now
5665                || Opaque3dBinKey {
5666                    asset_id: AssetId::<Mesh>::invalid().untyped(),
5667                },
5668                #[cfg(feature = "2d")]
5669                PipelineMode::Camera3d,
5670                ParticleRenderAlphaMaskPipelineKey::Opaque,
5671                &mut change_tick,
5672            );
5673        }
5674    }
5675}
5676
5677/// Once a child effect is batched, and therefore passed validations to be
5678/// updated and rendered this frame, dispatch a new GPU operation to fill the
5679/// indirect dispatch args of its init pass based on the number of GPU events
5680/// emitted in the previous frame and stored in its event buffer.
5681pub fn queue_init_indirect_workgroup_update(
5682    q_cached_effects: Query<(
5683        Entity,
5684        &CachedChildInfo,
5685        &CachedEffectEvents,
5686        &CachedReadyState,
5687    )>,
5688    mut init_fill_dispatch_queue: ResMut<InitFillDispatchQueue>,
5689) {
5690    debug_assert_eq!(
5691        GpuChildInfo::min_size().get() % 4,
5692        0,
5693        "Invalid GpuChildInfo alignment."
5694    );
5695
5696    // Schedule some GPU buffer operation to update the number of workgroups to
5697    // dispatch during the indirect init pass of this effect based on the number of
5698    // GPU spawn events written in its buffer.
5699    for (entity, cached_child_info, cached_effect_events, cached_ready_state) in &q_cached_effects {
5700        if !cached_ready_state.is_ready() {
5701            trace!(
5702                "[Effect {:?}] Skipping init_fill_dispatch.enqueue() because effect is not ready.",
5703                entity
5704            );
5705            continue;
5706        }
5707        let init_indirect_dispatch_index = cached_effect_events.init_indirect_dispatch_index;
5708        let global_child_index = cached_child_info.global_child_index;
5709        trace!(
5710            "[Effect {:?}] init_fill_dispatch.enqueue(): src:global_child_index={} dst:init_indirect_dispatch_index={}",
5711            entity,
5712            global_child_index,
5713            init_indirect_dispatch_index,
5714        );
5715        assert!(global_child_index != u32::MAX);
5716        init_fill_dispatch_queue.enqueue(global_child_index, init_indirect_dispatch_index);
5717    }
5718}
5719
5720/// Prepare GPU resources for effect rendering.
5721///
5722/// This system runs in the [`RenderSet::PrepareResources`] render set, after
5723/// Bevy has updated the [`ViewUniforms`], which need to be referenced to get
5724/// access to the current camera view.
5725pub(crate) fn prepare_gpu_resources(
5726    mut effects_meta: ResMut<EffectsMeta>,
5727    //mut effect_cache: ResMut<EffectCache>,
5728    mut event_cache: ResMut<EventCache>,
5729    mut effect_bind_groups: ResMut<EffectBindGroups>,
5730    mut sort_bind_groups: ResMut<SortBindGroups>,
5731    render_device: Res<RenderDevice>,
5732    render_queue: Res<RenderQueue>,
5733    view_uniforms: Res<ViewUniforms>,
5734    render_pipeline: Res<ParticlesRenderPipeline>,
5735    pipeline_cache: Res<PipelineCache>,
5736) {
5737    // Get the binding for the ViewUniform, the uniform data structure containing
5738    // the Camera data for the current view. If not available, we cannot render
5739    // anything.
5740    let Some(view_binding) = view_uniforms.uniforms.binding() else {
5741        return;
5742    };
5743
5744    // Upload simulation parameters for this frame
5745    let prev_buffer_id = effects_meta.sim_params_uniforms.buffer().map(|b| b.id());
5746    effects_meta
5747        .sim_params_uniforms
5748        .write_buffer(&render_device, &render_queue);
5749    if prev_buffer_id != effects_meta.sim_params_uniforms.buffer().map(|b| b.id()) {
5750        // Buffer changed, invalidate bind groups
5751        effects_meta.update_sim_params_bind_group = None;
5752        effects_meta.indirect_sim_params_bind_group = None;
5753    }
5754
5755    // Create the bind group for the camera/view parameters
5756    // FIXME - Not here!
5757    effects_meta.view_bind_group = Some(render_device.create_bind_group(
5758        "hanabi:bind_group_camera_view",
5759        &pipeline_cache.get_bind_group_layout(&render_pipeline.view_layout_desc),
5760        &[
5761            BindGroupEntry {
5762                binding: 0,
5763                resource: view_binding,
5764            },
5765            BindGroupEntry {
5766                binding: 1,
5767                resource: effects_meta.sim_params_uniforms.binding().unwrap(),
5768            },
5769        ],
5770    ));
5771
5772    // Re-/allocate the draw indirect args buffer if needed
5773    if effects_meta
5774        .draw_indirect_buffer
5775        .allocate_gpu(&render_device, &render_queue)
5776    {
5777        // All those bind groups use the buffer so need to be re-created
5778        trace!("*** Draw indirect args buffer re-allocated; clearing all bind groups using it.");
5779        effects_meta.update_sim_params_bind_group = None;
5780        effects_meta.indirect_metadata_bind_group = None;
5781    }
5782
5783    // Re-/allocate any GPU buffer if needed
5784    //effect_cache.prepare_buffers(&render_device, &render_queue, &mut
5785    // effect_bind_groups);
5786    event_cache.prepare_buffers(&render_device, &render_queue, &mut effect_bind_groups);
5787    sort_bind_groups.prepare_buffers(&render_device);
5788    if effects_meta
5789        .dispatch_indirect_buffer
5790        .prepare_buffers(&render_device)
5791    {
5792        // All those bind groups use the buffer so need to be re-created
5793        trace!("*** Dispatch indirect buffer for update pass re-allocated; clearing all bind groups using it.");
5794        effect_bind_groups.particle_slabs.clear();
5795    }
5796}
5797
5798/// Update the [`GpuEffectMetadata`] of all the effects queued for update/render
5799/// this frame.
5800///
5801/// By this point, all effects should have a [`CachedEffectMetadata`] with a
5802/// valid allocation in the GPU table for a [`GpuEffectMetadata`] entry. This
5803/// system actually synchronize the CPU value with the GPU one in case of
5804/// change.
5805pub(crate) fn prepare_effect_metadata(
5806    render_device: Res<RenderDevice>,
5807    render_queue: Res<RenderQueue>,
5808    mut q_effects: Query<(
5809        MainEntity,
5810        Ref<ExtractedEffect>,
5811        Ref<CachedEffect>,
5812        Ref<DispatchBufferIndices>,
5813        Option<Ref<CachedChildInfo>>,
5814        Option<Ref<CachedParentInfo>>,
5815        Option<Ref<CachedDrawIndirectArgs>>,
5816        Option<Ref<CachedEffectEvents>>,
5817        &mut CachedEffectMetadata,
5818    )>,
5819    mut effects_meta: ResMut<EffectsMeta>,
5820    mut effect_bind_groups: ResMut<EffectBindGroups>,
5821) {
5822    #[cfg(feature = "trace")]
5823    let _span = bevy::log::info_span!("prepare_effect_metadata").entered();
5824    trace!("prepare_effect_metadata");
5825
5826    for (
5827        main_entity,
5828        extracted_effect,
5829        cached_effect,
5830        dispatch_buffer_indices,
5831        maybe_cached_child_info,
5832        maybe_cached_parent_info,
5833        maybe_cached_draw_indirect_args,
5834        maybe_cached_effect_events,
5835        mut cached_effect_metadata,
5836    ) in &mut q_effects
5837    {
5838        // Check if anything relevant to GpuEffectMetadata changed this frame; otherwise
5839        // early out and skip this effect.
5840        let is_changed_ee = extracted_effect.is_changed();
5841        let is_changed_ce = cached_effect.is_changed();
5842        let is_changed_dbi = dispatch_buffer_indices.is_changed();
5843        let is_changed_cci = maybe_cached_child_info
5844            .as_ref()
5845            .map(|cci| cci.is_changed())
5846            .unwrap_or(false);
5847        let is_changed_cpi = maybe_cached_parent_info
5848            .as_ref()
5849            .map(|cpi| cpi.is_changed())
5850            .unwrap_or(false);
5851        let is_changed_cdia = maybe_cached_draw_indirect_args
5852            .as_ref()
5853            .map(|cdia| cdia.is_changed())
5854            .unwrap_or(false);
5855        let is_changed_cee = maybe_cached_effect_events
5856            .as_ref()
5857            .map(|cee| cee.is_changed())
5858            .unwrap_or(false);
5859        trace!(
5860            "Preparting GpuEffectMetadata for effect {:?}: is_changed[] = {} {} {} {} {} {} {}",
5861            main_entity,
5862            is_changed_ee,
5863            is_changed_ce,
5864            is_changed_dbi,
5865            is_changed_cci,
5866            is_changed_cpi,
5867            is_changed_cdia,
5868            is_changed_cee
5869        );
5870        if !is_changed_ee
5871            && !is_changed_ce
5872            && !is_changed_dbi
5873            && !is_changed_cci
5874            && !is_changed_cpi
5875            && !is_changed_cdia
5876            && !is_changed_cee
5877        {
5878            continue;
5879        }
5880
5881        let capacity = cached_effect.slice.len();
5882
5883        // Global and local indices of this effect as a child of another (parent) effect
5884        let (global_child_index, local_child_index) = maybe_cached_child_info
5885            .map(|cci| (cci.global_child_index, cci.local_child_index))
5886            .unwrap_or((u32::MAX, u32::MAX));
5887
5888        // Base index of all children of this (parent) effect
5889        let base_child_index = maybe_cached_parent_info
5890            .map(|cpi| {
5891                debug_assert_eq!(
5892                    cpi.byte_range.start % GpuChildInfo::SHADER_SIZE.get() as u32,
5893                    0
5894                );
5895                cpi.byte_range.start / GpuChildInfo::SHADER_SIZE.get() as u32
5896            })
5897            .unwrap_or(u32::MAX);
5898
5899        let particle_stride = extracted_effect.particle_layout.min_binding_size32().get() / 4;
5900        let sort_key_offset = extracted_effect
5901            .particle_layout
5902            .byte_offset(Attribute::RIBBON_ID)
5903            .map(|byte_offset| byte_offset / 4)
5904            .unwrap_or(u32::MAX);
5905        let sort_key2_offset = extracted_effect
5906            .particle_layout
5907            .byte_offset(Attribute::AGE)
5908            .map(|byte_offset| byte_offset / 4)
5909            .unwrap_or(u32::MAX);
5910
5911        let gpu_effect_metadata = GpuEffectMetadata {
5912            capacity,
5913            alive_count: 0,
5914            max_update: 0,
5915            max_spawn: capacity,
5916            indirect_write_index: 0,
5917            indirect_dispatch_index: dispatch_buffer_indices
5918                .update_dispatch_indirect_buffer_row_index,
5919            indirect_draw_index: maybe_cached_draw_indirect_args
5920                .map(|cdia| cdia.get_row().0)
5921                .unwrap_or(u32::MAX),
5922            init_indirect_dispatch_index: maybe_cached_effect_events
5923                .map(|cee| cee.init_indirect_dispatch_index)
5924                .unwrap_or(u32::MAX),
5925            local_child_index,
5926            global_child_index,
5927            base_child_index,
5928            particle_stride,
5929            sort_key_offset,
5930            sort_key2_offset,
5931            ..default()
5932        };
5933
5934        // Insert of update entry in GPU buffer table
5935        assert!(cached_effect_metadata.table_id.is_valid());
5936        if gpu_effect_metadata != cached_effect_metadata.metadata {
5937            effects_meta
5938                .effect_metadata_buffer
5939                .update(cached_effect_metadata.table_id, gpu_effect_metadata);
5940
5941            cached_effect_metadata.metadata = gpu_effect_metadata;
5942
5943            // This triggers on all new spawns and annoys everyone; silence until we can at
5944            // least warn only on non-first-spawn, and ideally split indirect data from that
5945            // struct so we don't overwrite it and solve the issue.
5946            debug!(
5947                "Updated metadata entry {} for effect {:?}, this will reset it.",
5948                cached_effect_metadata.table_id.0, main_entity
5949            );
5950        }
5951    }
5952
5953    // Once all EffectMetadata values are written, schedule a GPU upload
5954    if effects_meta
5955        .effect_metadata_buffer
5956        .allocate_gpu(render_device.as_ref(), render_queue.as_ref())
5957    {
5958        // All those bind groups use the buffer so need to be re-created
5959        trace!("*** Effect metadata buffer re-allocated; clearing all bind groups using it.");
5960        effects_meta.indirect_metadata_bind_group = None;
5961        effect_bind_groups.init_metadata_bind_groups.clear();
5962        effect_bind_groups.update_metadata_bind_groups.clear();
5963    }
5964}
5965
5966/// Read the queued init fill dispatch operations, batch them together by
5967/// contiguous source and destination entries in the buffers, and enqueue
5968/// corresponding GPU buffer fill dispatch operations for all batches.
5969///
5970/// This system runs after the GPU buffers have been (re-)allocated in
5971/// [`prepare_gpu_resources()`], so that it can read the new buffer IDs and
5972/// reference them from the generic [`GpuBufferOperationQueue`].
5973pub(crate) fn queue_init_fill_dispatch_ops(
5974    event_cache: Res<EventCache>,
5975    render_device: Res<RenderDevice>,
5976    render_queue: Res<RenderQueue>,
5977    mut init_fill_dispatch_queue: ResMut<InitFillDispatchQueue>,
5978    mut gpu_buffer_operations: ResMut<GpuBufferOperations>,
5979) {
5980    // Submit all queued init fill dispatch operations with the proper buffers
5981    if !init_fill_dispatch_queue.is_empty() {
5982        let src_buffer = event_cache.child_infos().buffer();
5983        let dst_buffer = event_cache.init_indirect_dispatch_buffer();
5984        if let (Some(src_buffer), Some(dst_buffer)) = (src_buffer, dst_buffer) {
5985            init_fill_dispatch_queue.submit(src_buffer, dst_buffer, &mut gpu_buffer_operations);
5986        } else {
5987            if src_buffer.is_none() {
5988                warn!("Event cache has no allocated GpuChildInfo buffer, but there's {} init fill dispatch operation(s) queued. Ignoring those operations. This will prevent child particles from spawning.", init_fill_dispatch_queue.queue.len());
5989            }
5990            if dst_buffer.is_none() {
5991                warn!("Event cache has no allocated GpuDispatchIndirect buffer, but there's {} init fill dispatch operation(s) queued. Ignoring those operations. This will prevent child particles from spawning.", init_fill_dispatch_queue.queue.len());
5992            }
5993        }
5994    }
5995
5996    // Once all GPU operations for this frame are enqueued, upload them to GPU
5997    gpu_buffer_operations.end_frame(&render_device, &render_queue);
5998}
5999
6000#[derive(SystemParam)]
6001pub struct PipelineParams<'w, 's> {
6002    dispatch_indirect_pipeline: Res<'w, DispatchIndirectPipeline>,
6003    utils_pipeline: Res<'w, UtilsPipeline>,
6004    init_pipeline: Res<'w, ParticlesInitPipeline>,
6005    update_pipeline: Res<'w, ParticlesUpdatePipeline>,
6006    render_pipeline: ResMut<'w, ParticlesRenderPipeline>,
6007    marker: PhantomData<&'s usize>,
6008}
6009
6010pub(crate) fn prepare_bind_groups(
6011    mut effects_meta: ResMut<EffectsMeta>,
6012    mut effect_cache: ResMut<EffectCache>,
6013    mut event_cache: ResMut<EventCache>,
6014    mut effect_bind_groups: ResMut<EffectBindGroups>,
6015    mut property_bind_groups: ResMut<PropertyBindGroups>,
6016    mut sort_bind_groups: ResMut<SortBindGroups>,
6017    property_cache: Res<PropertyCache>,
6018    sorted_effect_batched: Res<SortedEffectBatches>,
6019    render_device: Res<RenderDevice>,
6020    pipeline_cache: Res<PipelineCache>,
6021    pipelines: PipelineParams,
6022    gpu_images: Res<RenderAssets<GpuImage>>,
6023    mut gpu_buffer_operation_queue: ResMut<GpuBufferOperations>,
6024) {
6025    // We can't simulate nor render anything without at least the spawner buffer
6026    if effects_meta.spawner_buffer.is_empty() {
6027        return;
6028    }
6029    let Some(spawner_buffer) = effects_meta.spawner_buffer.buffer().cloned() else {
6030        return;
6031    };
6032
6033    // Workaround for too many params in system (TODO: refactor to split work?)
6034    let dispatch_indirect_pipeline = pipelines.dispatch_indirect_pipeline.into_inner();
6035    let utils_pipeline = pipelines.utils_pipeline.into_inner();
6036    let init_pipeline = pipelines.init_pipeline.into_inner();
6037    let update_pipeline = pipelines.update_pipeline.into_inner();
6038    let render_pipeline = pipelines.render_pipeline.into_inner();
6039
6040    // Ensure child_infos@3 bind group for the indirect pass is available if needed.
6041    // This returns `None` if the buffer is not ready, either because it's not
6042    // created yet or because it's not needed (no child effect).
6043    event_cache.ensure_indirect_child_info_buffer_bind_group(&render_device);
6044
6045    {
6046        #[cfg(feature = "trace")]
6047        let _span = bevy::log::info_span!("shared_bind_groups").entered();
6048
6049        // Make a copy of the buffer IDs before borrowing effects_meta mutably in the
6050        // loop below. Also allows earlying out before doing any work in case some
6051        // buffer is missing.
6052        let Some(spawner_buffer) = effects_meta.spawner_buffer.buffer().cloned() else {
6053            return;
6054        };
6055
6056        // Create the sim_params@0 bind group for the global simulation parameters,
6057        // which is shared by the init and update passes.
6058        if effects_meta.update_sim_params_bind_group.is_none() {
6059            if let Some(draw_indirect_buffer) = effects_meta.draw_indirect_buffer.buffer() {
6060                effects_meta.update_sim_params_bind_group = Some(render_device.create_bind_group(
6061                    "hanabi:bind_group:vfx_update:sim_params@0",
6062                    &pipeline_cache.get_bind_group_layout(&update_pipeline.sim_params_layout_desc),
6063                    &[
6064                        // @group(0) @binding(0) var<uniform> sim_params : SimParams;
6065                        BindGroupEntry {
6066                            binding: 0,
6067                            resource: effects_meta.sim_params_uniforms.binding().unwrap(),
6068                        },
6069                        // @group(0) @binding(1) var<storage, read_write> draw_indirect_buffer :
6070                        // array<DrawIndexedIndirectArgs>;
6071                        BindGroupEntry {
6072                            binding: 1,
6073                            resource: draw_indirect_buffer.as_entire_binding(),
6074                        },
6075                    ],
6076                ));
6077            } else {
6078                debug!("Cannot allocate bind group for vfx_update:sim_params@0 - draw_indirect_buffer not ready");
6079            }
6080        }
6081        if effects_meta.indirect_sim_params_bind_group.is_none() {
6082            effects_meta.indirect_sim_params_bind_group = Some(render_device.create_bind_group(
6083                "hanabi:bind_group:vfx_indirect:sim_params@0",
6084                &pipeline_cache.get_bind_group_layout(&init_pipeline.sim_params_layout_desc), // FIXME - Shared with init
6085                &[
6086                    // @group(0) @binding(0) var<uniform> sim_params : SimParams;
6087                    BindGroupEntry {
6088                        binding: 0,
6089                        resource: effects_meta.sim_params_uniforms.binding().unwrap(),
6090                    },
6091                ],
6092            ));
6093        }
6094
6095        // Create the @1 bind group for the indirect dispatch preparation pass of all
6096        // effects at once
6097        effects_meta.indirect_metadata_bind_group = match (
6098            effects_meta.effect_metadata_buffer.buffer(),
6099            effects_meta.dispatch_indirect_buffer.buffer(),
6100            effects_meta.draw_indirect_buffer.buffer(),
6101        ) {
6102            (
6103                Some(effect_metadata_buffer),
6104                Some(dispatch_indirect_buffer),
6105                Some(draw_indirect_buffer),
6106            ) => {
6107                // Base bind group for indirect pass
6108                Some(render_device.create_bind_group(
6109                    "hanabi:bind_group:vfx_indirect:metadata@1",
6110                    &pipeline_cache.get_bind_group_layout(
6111                        &dispatch_indirect_pipeline.effect_metadata_bind_group_layout_desc,
6112                    ),
6113                    &[
6114                        // @group(1) @binding(0) var<storage, read_write> effect_metadata_buffer :
6115                        // array<u32>;
6116                        BindGroupEntry {
6117                            binding: 0,
6118                            resource: effect_metadata_buffer.as_entire_binding(),
6119                        },
6120                        // @group(1) @binding(1) var<storage, read_write> dispatch_indirect_buffer
6121                        // : array<DispatchIndirectArgs>;
6122                        BindGroupEntry {
6123                            binding: 1,
6124                            resource: dispatch_indirect_buffer.as_entire_binding(),
6125                        },
6126                        // @group(1) @binding(2) var<storage, read_write> draw_indirect_buffer :
6127                        // array<u32>;
6128                        BindGroupEntry {
6129                            binding: 2,
6130                            resource: draw_indirect_buffer.as_entire_binding(),
6131                        },
6132                    ],
6133                ))
6134            }
6135
6136            // Some buffer is not yet available, can't create the bind group
6137            _ => None,
6138        };
6139
6140        // Create the @2 bind group for the indirect dispatch preparation pass of all
6141        // effects at once
6142        if effects_meta.indirect_spawner_bind_group.is_none() {
6143            let bind_group = render_device.create_bind_group(
6144                "hanabi:bind_group:vfx_indirect:spawner@2",
6145                &pipeline_cache.get_bind_group_layout(
6146                    &dispatch_indirect_pipeline.spawner_bind_group_layout_desc,
6147                ),
6148                &[
6149                    // @group(2) @binding(0) var<storage, read> spawner_buffer : array<Spawner>;
6150                    BindGroupEntry {
6151                        binding: 0,
6152                        resource: BindingResource::Buffer(BufferBinding {
6153                            buffer: &spawner_buffer,
6154                            offset: 0,
6155                            size: None,
6156                        }),
6157                    },
6158                ],
6159            );
6160
6161            effects_meta.indirect_spawner_bind_group = Some(bind_group);
6162        }
6163    }
6164
6165    // Create the per-slab bind groups
6166    trace!("Create per-slab bind groups...");
6167    for (slab_index, particle_slab) in effect_cache.slabs().iter().enumerate() {
6168        #[cfg(feature = "trace")]
6169        let _span_buffer = bevy::log::info_span!("create_buffer_bind_groups").entered();
6170
6171        let Some(particle_slab) = particle_slab else {
6172            trace!(
6173                "Particle slab index #{} has no allocated EffectBuffer, skipped.",
6174                slab_index
6175            );
6176            continue;
6177        };
6178
6179        // Ensure all effects in this batch have a bind group for the entire buffer of
6180        // the group, since the update phase runs on an entire group/buffer at once,
6181        // with all the effect instances in it batched together.
6182        trace!("effect particle slab_index=#{}", slab_index);
6183        effect_bind_groups
6184            .particle_slabs
6185            .entry(SlabId::new(slab_index as u32))
6186            .or_insert_with(|| {
6187                // Bind group particle@1 for render pass
6188                trace!("Creating particle@1 bind group for buffer #{slab_index} in render pass");
6189                let spawner_min_binding_size = GpuSpawnerParams::aligned_size(
6190                    render_device.limits().min_storage_buffer_offset_alignment,
6191                );
6192                let entries = [
6193                    // @group(1) @binding(0) var<storage, read> particle_buffer : ParticleBuffer;
6194                    BindGroupEntry {
6195                        binding: 0,
6196                        resource: particle_slab.as_entire_binding_particle(),
6197                    },
6198                    // @group(1) @binding(1) var<storage, read> indirect_buffer : IndirectBuffer;
6199                    BindGroupEntry {
6200                        binding: 1,
6201                        resource: particle_slab.as_entire_binding_indirect(),
6202                    },
6203                    // @group(1) @binding(2) var<storage, read> spawner : Spawner;
6204                    BindGroupEntry {
6205                        binding: 2,
6206                        resource: BindingResource::Buffer(BufferBinding {
6207                            buffer: &spawner_buffer,
6208                            offset: 0,
6209                            size: Some(spawner_min_binding_size),
6210                        }),
6211                    },
6212                ];
6213                let render = render_device.create_bind_group(
6214                    &format!("hanabi:bind_group:render:particles@1:vfx{slab_index}")[..],
6215                    particle_slab.render_particles_buffer_layout(),
6216                    &entries[..],
6217                );
6218
6219                BufferBindGroups { render }
6220            });
6221    }
6222
6223    // Create bind groups for queued GPU buffer operations
6224    gpu_buffer_operation_queue.create_bind_groups(&render_device, utils_pipeline);
6225
6226    // Create the per-effect bind groups
6227    let spawner_buffer_binding_size =
6228        NonZeroU64::new(effects_meta.spawner_buffer.aligned_size() as u64).unwrap();
6229    for effect_batch in sorted_effect_batched.iter() {
6230        #[cfg(feature = "trace")]
6231        let _span_buffer = bevy::log::info_span!("create_batch_bind_groups").entered();
6232
6233        // Create the property bind group @2 if needed
6234        if let Some(property_key) = &effect_batch.property_key {
6235            if let Err(err) = property_bind_groups.ensure_exists(
6236                property_key,
6237                &property_cache,
6238                &spawner_buffer,
6239                spawner_buffer_binding_size,
6240                &render_device,
6241                &pipeline_cache,
6242            ) {
6243                error!("Failed to create property bind group for effect batch: {err:?}");
6244                continue;
6245            }
6246        } else if let Err(err) = property_bind_groups.ensure_exists_no_property(
6247            &property_cache,
6248            &spawner_buffer,
6249            spawner_buffer_binding_size,
6250            &render_device,
6251            &pipeline_cache,
6252        ) {
6253            error!("Failed to create property bind group for effect batch: {err:?}");
6254            continue;
6255        }
6256
6257        // Bind group particle@1 for the simulate compute shaders (init and udpate) to
6258        // simulate particles.
6259        if effect_cache
6260            .create_particle_sim_bind_group(
6261                &effect_batch.slab_id,
6262                &render_device,
6263                effect_batch.particle_layout.min_binding_size32(),
6264                effect_batch.parent_min_binding_size,
6265                effect_batch.parent_binding_source.as_ref(),
6266                &pipeline_cache,
6267            )
6268            .is_err()
6269        {
6270            error!("No particle buffer allocated for effect batch.");
6271            continue;
6272        }
6273
6274        // Bind group @3 of init pass
6275        // FIXME - this is instance-dependent, not buffer-dependent
6276        {
6277            let consume_gpu_spawn_events = effect_batch
6278                .layout_flags
6279                .contains(LayoutFlags::CONSUME_GPU_SPAWN_EVENTS);
6280            let consume_event_buffers = if let BatchSpawnInfo::GpuSpawner { .. } =
6281                effect_batch.spawn_info
6282            {
6283                assert!(consume_gpu_spawn_events);
6284                let cached_effect_events = effect_batch.cached_effect_events.as_ref().unwrap();
6285                Some(ConsumeEventBuffers {
6286                    child_infos_buffer: event_cache.child_infos_buffer().unwrap(),
6287                    events: BufferSlice {
6288                        buffer: event_cache
6289                            .get_buffer(cached_effect_events.buffer_index)
6290                            .unwrap(),
6291                        // Note: event range is in u32 count, not bytes
6292                        offset: cached_effect_events.range.start * 4,
6293                        size: NonZeroU32::new(cached_effect_events.range.len() as u32 * 4).unwrap(),
6294                    },
6295                })
6296            } else {
6297                assert!(!consume_gpu_spawn_events);
6298                None
6299            };
6300            let Some(init_metadata_layout_desc) =
6301                effect_cache.metadata_init_bind_group_layout_desc(consume_gpu_spawn_events)
6302            else {
6303                continue;
6304            };
6305            if effect_bind_groups
6306                .get_or_create_init_metadata(
6307                    effect_batch,
6308                    &render_device,
6309                    &pipeline_cache.get_bind_group_layout(init_metadata_layout_desc),
6310                    effects_meta.effect_metadata_buffer.buffer().unwrap(),
6311                    consume_event_buffers,
6312                )
6313                .is_err()
6314            {
6315                continue;
6316            }
6317        }
6318
6319        // Bind group @3 of update pass
6320        // FIXME - this is instance-dependent, not buffer-dependent#
6321        {
6322            let num_event_buffers = effect_batch.child_event_buffers.len() as u32;
6323
6324            let Some(update_metadata_layout_desc) =
6325                effect_cache.metadata_update_bind_group_layout_desc(num_event_buffers)
6326            else {
6327                continue;
6328            };
6329            if effect_bind_groups
6330                .get_or_create_update_metadata(
6331                    effect_batch,
6332                    &render_device,
6333                    &pipeline_cache.get_bind_group_layout(update_metadata_layout_desc),
6334                    effects_meta.effect_metadata_buffer.buffer().unwrap(),
6335                    event_cache.child_infos_buffer(),
6336                    &effect_batch.child_event_buffers[..],
6337                )
6338                .is_err()
6339            {
6340                continue;
6341            }
6342        }
6343
6344        if effect_batch.layout_flags.contains(LayoutFlags::RIBBONS) {
6345            let effect_buffer = effect_cache.get_slab(&effect_batch.slab_id).unwrap();
6346
6347            // Bind group @0 of sort-fill pass
6348            let particle_buffer = effect_buffer.particle_buffer();
6349            let indirect_index_buffer = effect_buffer.indirect_index_buffer();
6350            let effect_metadata_buffer = effects_meta.effect_metadata_buffer.buffer().unwrap();
6351            if let Err(err) = sort_bind_groups.ensure_sort_fill_bind_group(
6352                &effect_batch.particle_layout,
6353                particle_buffer,
6354                indirect_index_buffer,
6355                effect_metadata_buffer,
6356                &spawner_buffer,
6357                &pipeline_cache,
6358            ) {
6359                error!(
6360                    "Failed to create sort-fill bind group @0 for ribbon effect: {:?}",
6361                    err
6362                );
6363                continue;
6364            }
6365
6366            // Bind group @0 of sort pass
6367            if let Err(err) = sort_bind_groups.ensure_sort_bind_group(&pipeline_cache) {
6368                error!(
6369                    "Failed to create sort bind group @0 for ribbon effect: {:?}",
6370                    err
6371                );
6372                continue;
6373            }
6374
6375            // Bind group @0 of sort-copy pass
6376            let indirect_index_buffer = effect_buffer.indirect_index_buffer();
6377            if let Err(err) = sort_bind_groups.ensure_sort_copy_bind_group(
6378                indirect_index_buffer,
6379                effect_metadata_buffer,
6380                &spawner_buffer,
6381                &pipeline_cache,
6382            ) {
6383                error!(
6384                    "Failed to create sort-copy bind group @0 for ribbon effect: {:?}",
6385                    err
6386                );
6387                continue;
6388            }
6389        }
6390
6391        // Ensure the particle texture(s) are available as GPU resources and that a bind
6392        // group for them exists
6393        // FIXME fix this insert+get below
6394        if !effect_batch.texture_layout.layout.is_empty() {
6395            // This should always be available, as this is cached into the render pipeline
6396            // just before we start specializing it.
6397            let Some(material_bind_group_layout_desc) =
6398                render_pipeline.get_material(&effect_batch.texture_layout)
6399            else {
6400                error!(
6401                    "Failed to find material bind group layout for particle slab #{}",
6402                    effect_batch.slab_id.index()
6403                );
6404                continue;
6405            };
6406
6407            // TODO = move
6408            let material = Material {
6409                layout: effect_batch.texture_layout.clone(),
6410                textures: effect_batch.textures.iter().map(|h| h.id()).collect(),
6411            };
6412            assert_eq!(material.layout.layout.len(), material.textures.len());
6413
6414            //let bind_group_entries = material.make_entries(&gpu_images).unwrap();
6415            let Ok(bind_group_entries) = material.make_entries(&gpu_images) else {
6416                trace!(
6417                    "Temporarily ignoring material {:?} due to missing image(s)",
6418                    material
6419                );
6420                continue;
6421            };
6422
6423            effect_bind_groups
6424                .material_bind_groups
6425                .entry(material.clone())
6426                .or_insert_with(|| {
6427                    debug!("Creating material bind group for material {:?}", material);
6428                    render_device.create_bind_group(
6429                        &format!(
6430                            "hanabi:material_bind_group_{}",
6431                            material.layout.layout.len()
6432                        )[..],
6433                        &pipeline_cache.get_bind_group_layout(material_bind_group_layout_desc),
6434                        &bind_group_entries[..],
6435                    )
6436                });
6437        }
6438    }
6439}
6440
6441type DrawEffectsSystemState = SystemState<(
6442    SRes<EffectsMeta>,
6443    SRes<EffectBindGroups>,
6444    SRes<PipelineCache>,
6445    SRes<RenderAssets<RenderMesh>>,
6446    SRes<MeshAllocator>,
6447    SQuery<Read<ViewUniformOffset>>,
6448    SRes<SortedEffectBatches>,
6449    SQuery<Read<EffectDrawBatch>>,
6450)>;
6451
6452/// Draw function for rendering all active effects for the current frame.
6453///
6454/// Effects are rendered in the [`Transparent2d`] phase of the main 2D pass,
6455/// and the [`Transparent3d`] phase of the main 3D pass.
6456pub(crate) struct DrawEffects {
6457    params: DrawEffectsSystemState,
6458}
6459
6460impl DrawEffects {
6461    pub fn new(world: &mut World) -> Self {
6462        Self {
6463            params: SystemState::new(world),
6464        }
6465    }
6466}
6467
6468/// Draw all particles of a single effect in view, in 2D or 3D.
6469///
6470/// FIXME: use pipeline ID to look up which group index it is.
6471fn draw<'w>(
6472    world: &'w World,
6473    pass: &mut TrackedRenderPass<'w>,
6474    view: Entity,
6475    entity: (Entity, MainEntity),
6476    pipeline_id: CachedRenderPipelineId,
6477    params: &mut DrawEffectsSystemState,
6478) {
6479    let (
6480        effects_meta,
6481        effect_bind_groups,
6482        pipeline_cache,
6483        meshes,
6484        mesh_allocator,
6485        views,
6486        sorted_effect_batches,
6487        effect_draw_batches,
6488    ) = params.get(world);
6489    let view_uniform = views.get(view).unwrap();
6490    let effects_meta = effects_meta.into_inner();
6491    let effect_bind_groups = effect_bind_groups.into_inner();
6492    let meshes = meshes.into_inner();
6493    let mesh_allocator = mesh_allocator.into_inner();
6494    let effect_draw_batch = effect_draw_batches.get(entity.0).unwrap();
6495    let effect_batch = sorted_effect_batches
6496        .get(effect_draw_batch.effect_batch_index)
6497        .unwrap();
6498
6499    let Some(pipeline) = pipeline_cache.into_inner().get_render_pipeline(pipeline_id) else {
6500        return;
6501    };
6502
6503    trace!("render pass");
6504
6505    pass.set_render_pipeline(pipeline);
6506
6507    let Some(render_mesh): Option<&RenderMesh> = meshes.get(effect_batch.mesh) else {
6508        return;
6509    };
6510    let Some(vertex_buffer_slice) = mesh_allocator.mesh_vertex_slice(&effect_batch.mesh) else {
6511        return;
6512    };
6513
6514    // Vertex buffer containing the particle model to draw. Generally a quad.
6515    // FIXME - need to upload "vertex_buffer_slice.range.start as i32" into
6516    // "base_vertex" in the indirect struct...
6517    pass.set_vertex_buffer(0, vertex_buffer_slice.buffer.slice(..));
6518
6519    // View properties (camera matrix, etc.)
6520    pass.set_bind_group(
6521        0,
6522        effects_meta.view_bind_group.as_ref().unwrap(),
6523        &[view_uniform.offset],
6524    );
6525
6526    // Particles buffer
6527    let spawner_base = effect_batch.spawner_base;
6528    let spawner_buffer_aligned = effects_meta.spawner_buffer.aligned_size();
6529    assert!(spawner_buffer_aligned >= GpuSpawnerParams::min_size().get() as usize);
6530    let spawner_offset = spawner_base * spawner_buffer_aligned as u32;
6531    pass.set_bind_group(
6532        1,
6533        effect_bind_groups
6534            .particle_render(&effect_batch.slab_id)
6535            .unwrap(),
6536        &[spawner_offset],
6537    );
6538
6539    // Particle texture
6540    // TODO = move
6541    let material = Material {
6542        layout: effect_batch.texture_layout.clone(),
6543        textures: effect_batch.textures.iter().map(|h| h.id()).collect(),
6544    };
6545    if !effect_batch.texture_layout.layout.is_empty() {
6546        if let Some(bind_group) = effect_bind_groups.material_bind_groups.get(&material) {
6547            pass.set_bind_group(2, bind_group, &[]);
6548        } else {
6549            // Texture(s) not ready; skip this drawing for now
6550            trace!(
6551                "Particle material bind group not available for batch slab_id={}. Skipping draw call.",
6552                effect_batch.slab_id.index(),
6553            );
6554            return;
6555        }
6556    }
6557
6558    let draw_indirect_index = effect_batch.draw_indirect_buffer_row_index.0;
6559    assert_eq!(GpuDrawIndexedIndirectArgs::SHADER_SIZE.get(), 20);
6560    let draw_indirect_offset =
6561        draw_indirect_index as u64 * GpuDrawIndexedIndirectArgs::SHADER_SIZE.get();
6562    trace!(
6563        "Draw up to {} particles with {} vertices per particle for batch from particle slab #{} \
6564            (effect_metadata_index={}, draw_indirect_offset={}B).",
6565        effect_batch.slice.len(),
6566        render_mesh.vertex_count,
6567        effect_batch.slab_id.index(),
6568        draw_indirect_index,
6569        draw_indirect_offset,
6570    );
6571
6572    let Some(indirect_buffer) = effects_meta.draw_indirect_buffer.buffer() else {
6573        trace!(
6574            "The draw indirect buffer containing the indirect draw args is not ready for batch slab_id=#{}. Skipping draw call.",
6575            effect_batch.slab_id.index(),
6576        );
6577        return;
6578    };
6579
6580    match render_mesh.buffer_info {
6581        RenderMeshBufferInfo::Indexed { index_format, .. } => {
6582            let Some(index_buffer_slice) = mesh_allocator.mesh_index_slice(&effect_batch.mesh)
6583            else {
6584                trace!(
6585                    "The index buffer for indexed rendering is not ready for batch slab_id=#{}. Skipping draw call.",
6586                    effect_batch.slab_id.index(),
6587                );
6588                return;
6589            };
6590
6591            pass.set_index_buffer(index_buffer_slice.buffer.slice(..), index_format);
6592            pass.draw_indexed_indirect(indirect_buffer, draw_indirect_offset);
6593        }
6594        RenderMeshBufferInfo::NonIndexed => {
6595            pass.draw_indirect(indirect_buffer, draw_indirect_offset);
6596        }
6597    }
6598}
6599
6600#[cfg(feature = "2d")]
6601impl Draw<Transparent2d> for DrawEffects {
6602    fn draw<'w>(
6603        &mut self,
6604        world: &'w World,
6605        pass: &mut TrackedRenderPass<'w>,
6606        view: Entity,
6607        item: &Transparent2d,
6608    ) -> Result<(), DrawError> {
6609        trace!("Draw<Transparent2d>: view={:?}", view);
6610        draw(
6611            world,
6612            pass,
6613            view,
6614            item.entity,
6615            item.pipeline,
6616            &mut self.params,
6617        );
6618        Ok(())
6619    }
6620}
6621
6622#[cfg(feature = "3d")]
6623impl Draw<Transparent3d> for DrawEffects {
6624    fn draw<'w>(
6625        &mut self,
6626        world: &'w World,
6627        pass: &mut TrackedRenderPass<'w>,
6628        view: Entity,
6629        item: &Transparent3d,
6630    ) -> Result<(), DrawError> {
6631        trace!("Draw<Transparent3d>: view={:?}", view);
6632        draw(
6633            world,
6634            pass,
6635            view,
6636            item.entity,
6637            item.pipeline,
6638            &mut self.params,
6639        );
6640        Ok(())
6641    }
6642}
6643
6644#[cfg(feature = "3d")]
6645impl Draw<AlphaMask3d> for DrawEffects {
6646    fn draw<'w>(
6647        &mut self,
6648        world: &'w World,
6649        pass: &mut TrackedRenderPass<'w>,
6650        view: Entity,
6651        item: &AlphaMask3d,
6652    ) -> Result<(), DrawError> {
6653        trace!("Draw<AlphaMask3d>: view={:?}", view);
6654        draw(
6655            world,
6656            pass,
6657            view,
6658            item.representative_entity,
6659            item.batch_set_key.pipeline,
6660            &mut self.params,
6661        );
6662        Ok(())
6663    }
6664}
6665
6666#[cfg(feature = "3d")]
6667impl Draw<Opaque3d> for DrawEffects {
6668    fn draw<'w>(
6669        &mut self,
6670        world: &'w World,
6671        pass: &mut TrackedRenderPass<'w>,
6672        view: Entity,
6673        item: &Opaque3d,
6674    ) -> Result<(), DrawError> {
6675        trace!("Draw<Opaque3d>: view={:?}", view);
6676        draw(
6677            world,
6678            pass,
6679            view,
6680            item.representative_entity,
6681            item.batch_set_key.pipeline,
6682            &mut self.params,
6683        );
6684        Ok(())
6685    }
6686}
6687
6688/// Render node to run the simulation sub-graph once per frame.
6689///
6690/// This node doesn't simulate anything by itself, but instead schedules the
6691/// simulation sub-graph, where other nodes like [`VfxSimulateNode`] do the
6692/// actual simulation.
6693///
6694/// The simulation sub-graph is scheduled to run before the [`CameraDriverNode`]
6695/// renders all the views, such that rendered views have access to the
6696/// just-simulated particles to render them.
6697///
6698/// [`CameraDriverNode`]: bevy::render::camera::CameraDriverNode
6699pub(crate) struct VfxSimulateDriverNode;
6700
6701impl Node for VfxSimulateDriverNode {
6702    fn run(
6703        &self,
6704        graph: &mut RenderGraphContext,
6705        _render_context: &mut RenderContext,
6706        _world: &World,
6707    ) -> Result<(), NodeRunError> {
6708        graph.run_sub_graph(
6709            crate::plugin::simulate_graph::HanabiSimulateGraph,
6710            vec![],
6711            None,
6712            Some("hanabi".to_string()),
6713        )?;
6714        Ok(())
6715    }
6716}
6717
6718#[derive(Debug, Clone, PartialEq, Eq)]
6719enum HanabiPipelineId {
6720    Invalid,
6721    Cached(CachedComputePipelineId),
6722}
6723
6724#[derive(Debug)]
6725pub(crate) enum ComputePipelineError {
6726    Queued,
6727    Creating,
6728    Error,
6729}
6730
6731impl From<&CachedPipelineState> for ComputePipelineError {
6732    fn from(value: &CachedPipelineState) -> Self {
6733        match value {
6734            CachedPipelineState::Queued => Self::Queued,
6735            CachedPipelineState::Creating(_) => Self::Creating,
6736            CachedPipelineState::Err(_) => Self::Error,
6737            _ => panic!("Trying to convert Ok state to error."),
6738        }
6739    }
6740}
6741
6742pub(crate) struct HanabiComputePass<'a> {
6743    /// Pipeline cache to fetch cached compute pipelines by ID.
6744    pipeline_cache: &'a PipelineCache,
6745    /// WGPU compute pass.
6746    compute_pass: ComputePass<'a>,
6747    /// Current pipeline (cached).
6748    pipeline_id: HanabiPipelineId,
6749}
6750
6751impl<'a> Deref for HanabiComputePass<'a> {
6752    type Target = ComputePass<'a>;
6753
6754    fn deref(&self) -> &Self::Target {
6755        &self.compute_pass
6756    }
6757}
6758
6759impl DerefMut for HanabiComputePass<'_> {
6760    fn deref_mut(&mut self) -> &mut Self::Target {
6761        &mut self.compute_pass
6762    }
6763}
6764
6765impl<'a> HanabiComputePass<'a> {
6766    pub fn new(pipeline_cache: &'a PipelineCache, compute_pass: ComputePass<'a>) -> Self {
6767        Self {
6768            pipeline_cache,
6769            compute_pass,
6770            pipeline_id: HanabiPipelineId::Invalid,
6771        }
6772    }
6773
6774    pub fn set_cached_compute_pipeline(
6775        &mut self,
6776        pipeline_id: CachedComputePipelineId,
6777    ) -> Result<(), ComputePipelineError> {
6778        if HanabiPipelineId::Cached(pipeline_id) == self.pipeline_id {
6779            trace!("set_cached_compute_pipeline() id={pipeline_id:?} -> already set; skipped");
6780            return Ok(());
6781        }
6782        trace!("set_cached_compute_pipeline() id={pipeline_id:?}");
6783        let Some(pipeline) = self.pipeline_cache.get_compute_pipeline(pipeline_id) else {
6784            let state = self.pipeline_cache.get_compute_pipeline_state(pipeline_id);
6785            if let CachedPipelineState::Err(err) = state {
6786                error!(
6787                    "Failed to find compute pipeline #{}: {:?}",
6788                    pipeline_id.id(),
6789                    err
6790                );
6791            } else {
6792                debug!("Compute pipeline not ready #{}", pipeline_id.id());
6793            }
6794            return Err(state.into());
6795        };
6796        self.compute_pass.set_pipeline(pipeline);
6797        self.pipeline_id = HanabiPipelineId::Cached(pipeline_id);
6798        Ok(())
6799    }
6800}
6801
6802/// Render node to run the simulation of all effects once per frame.
6803///
6804/// Runs inside the simulation sub-graph, looping over all extracted effect
6805/// batches to simulate them.
6806pub(crate) struct VfxSimulateNode {}
6807
6808impl VfxSimulateNode {
6809    /// Create a new node for simulating the effects of the given world.
6810    pub fn new(_world: &mut World) -> Self {
6811        Self {}
6812    }
6813
6814    /// Begin a new compute pass and return a wrapper with extra
6815    /// functionalities.
6816    pub fn begin_compute_pass<'encoder>(
6817        &self,
6818        label: &str,
6819        pipeline_cache: &'encoder PipelineCache,
6820        render_context: &'encoder mut RenderContext,
6821    ) -> HanabiComputePass<'encoder> {
6822        let compute_pass =
6823            render_context
6824                .command_encoder()
6825                .begin_compute_pass(&ComputePassDescriptor {
6826                    label: Some(label),
6827                    timestamp_writes: None,
6828                });
6829        HanabiComputePass::new(pipeline_cache, compute_pass)
6830    }
6831}
6832
6833impl Node for VfxSimulateNode {
6834    fn input(&self) -> Vec<SlotInfo> {
6835        vec![]
6836    }
6837
6838    fn update(&mut self, _world: &mut World) {}
6839
6840    fn run(
6841        &self,
6842        _graph: &mut RenderGraphContext,
6843        render_context: &mut RenderContext,
6844        world: &World,
6845    ) -> Result<(), NodeRunError> {
6846        trace!("VfxSimulateNode::run()");
6847
6848        let pipeline_cache = world.resource::<PipelineCache>();
6849        let effects_meta = world.resource::<EffectsMeta>();
6850        let effect_bind_groups = world.resource::<EffectBindGroups>();
6851        let property_bind_groups = world.resource::<PropertyBindGroups>();
6852        let sort_bind_groups = world.resource::<SortBindGroups>();
6853        let utils_pipeline = world.resource::<UtilsPipeline>();
6854        let effect_cache = world.resource::<EffectCache>();
6855        let event_cache = world.resource::<EventCache>();
6856        let gpu_buffer_operations = world.resource::<GpuBufferOperations>();
6857        let sorted_effect_batches = world.resource::<SortedEffectBatches>();
6858        let init_fill_dispatch_queue = world.resource::<InitFillDispatchQueue>();
6859
6860        // Make sure to schedule any buffer copy before accessing their content later in
6861        // the GPU commands below.
6862        {
6863            let command_encoder = render_context.command_encoder();
6864            effects_meta
6865                .dispatch_indirect_buffer
6866                .write_buffers(command_encoder);
6867            effects_meta
6868                .draw_indirect_buffer
6869                .write_buffer(command_encoder);
6870            effects_meta
6871                .effect_metadata_buffer
6872                .write_buffer(command_encoder);
6873            event_cache.write_buffers(command_encoder);
6874            sort_bind_groups.write_buffers(command_encoder);
6875        }
6876
6877        // Compute init fill dispatch pass - Fill the indirect dispatch structs for any
6878        // upcoming init pass of this frame, based on the GPU spawn events emitted by
6879        // the update pass of their parent effect during the previous frame.
6880        if let Some(queue_index) = init_fill_dispatch_queue.submitted_queue_index.as_ref() {
6881            gpu_buffer_operations.dispatch(
6882                *queue_index,
6883                render_context,
6884                utils_pipeline,
6885                Some("hanabi:init_indirect_fill_dispatch"),
6886            );
6887        }
6888
6889        // If there's no batch, there's nothing more to do. Avoid continuing because
6890        // some GPU resources are missing, which is expected when there's no effect but
6891        // is an error (and will log warnings/errors) otherwise.
6892        if sorted_effect_batches.is_empty() {
6893            return Ok(());
6894        }
6895
6896        // Compute init pass
6897        {
6898            trace!("init: loop over effect batches...");
6899
6900            let mut compute_pass =
6901                self.begin_compute_pass("hanabi:init", pipeline_cache, render_context);
6902
6903            // Bind group simparams@0 is common to everything, only set once per init pass
6904            compute_pass.set_bind_group(
6905                0,
6906                effects_meta
6907                    .indirect_sim_params_bind_group
6908                    .as_ref()
6909                    .unwrap(),
6910                &[],
6911            );
6912
6913            // Dispatch init compute jobs for all batches
6914            for effect_batch in sorted_effect_batches.iter() {
6915                // Do not dispatch any init work if there's nothing to spawn this frame for the
6916                // batch. Note that this hopefully should have been skipped earlier.
6917                {
6918                    let use_indirect_dispatch = effect_batch
6919                        .layout_flags
6920                        .contains(LayoutFlags::CONSUME_GPU_SPAWN_EVENTS);
6921                    match effect_batch.spawn_info {
6922                        BatchSpawnInfo::CpuSpawner { total_spawn_count } => {
6923                            assert!(!use_indirect_dispatch);
6924                            if total_spawn_count == 0 {
6925                                continue;
6926                            }
6927                        }
6928                        BatchSpawnInfo::GpuSpawner { .. } => {
6929                            assert!(use_indirect_dispatch);
6930                        }
6931                    }
6932                }
6933
6934                // Fetch bind group particle@1
6935                let Some(particle_bind_group) =
6936                    effect_cache.particle_sim_bind_group(&effect_batch.slab_id)
6937                else {
6938                    error!(
6939                        "Failed to find init particle@1 bind group for slab #{}",
6940                        effect_batch.slab_id.index()
6941                    );
6942                    continue;
6943                };
6944
6945                // Fetch bind group metadata@3
6946                let Some(metadata_bind_group) = effect_bind_groups
6947                    .init_metadata_bind_groups
6948                    .get(&effect_batch.slab_id)
6949                else {
6950                    error!(
6951                        "Failed to find init metadata@3 bind group for slab #{}",
6952                        effect_batch.slab_id.index()
6953                    );
6954                    continue;
6955                };
6956
6957                if compute_pass
6958                    .set_cached_compute_pipeline(effect_batch.init_and_update_pipeline_ids.init)
6959                    .is_err()
6960                {
6961                    continue;
6962                }
6963
6964                // Compute dynamic offsets
6965                let spawner_base = effect_batch.spawner_base;
6966                let spawner_aligned_size = effects_meta.spawner_buffer.aligned_size();
6967                debug_assert!(spawner_aligned_size >= GpuSpawnerParams::min_size().get() as usize);
6968                let spawner_offset = spawner_base * spawner_aligned_size as u32;
6969                let property_offset = effect_batch.property_offset;
6970
6971                // Setup init pass
6972                compute_pass.set_bind_group(1, particle_bind_group, &[]);
6973                let offsets = if let Some(property_offset) = property_offset {
6974                    vec![spawner_offset, property_offset]
6975                } else {
6976                    vec![spawner_offset]
6977                };
6978                compute_pass.set_bind_group(
6979                    2,
6980                    property_bind_groups
6981                        .get(effect_batch.property_key.as_ref())
6982                        .unwrap(),
6983                    &offsets[..],
6984                );
6985                compute_pass.set_bind_group(3, &metadata_bind_group.bind_group, &[]);
6986
6987                // Dispatch init job
6988                match effect_batch.spawn_info {
6989                    // Indirect dispatch via GPU spawn events
6990                    BatchSpawnInfo::GpuSpawner {
6991                        init_indirect_dispatch_index,
6992                        ..
6993                    } => {
6994                        assert!(effect_batch
6995                            .layout_flags
6996                            .contains(LayoutFlags::CONSUME_GPU_SPAWN_EVENTS));
6997
6998                        // Note: the indirect offset of a dispatch workgroup only needs
6999                        // 4-byte alignment
7000                        assert_eq!(GpuDispatchIndirectArgs::min_size().get(), 12);
7001                        let indirect_offset = init_indirect_dispatch_index as u64 * 12;
7002
7003                        trace!(
7004                            "record commands for indirect init pipeline of effect {:?} \
7005                                init_indirect_dispatch_index={} \
7006                                indirect_offset={} \
7007                                spawner_base={} \
7008                                spawner_offset={} \
7009                                property_key={:?}...",
7010                            effect_batch.handle,
7011                            init_indirect_dispatch_index,
7012                            indirect_offset,
7013                            spawner_base,
7014                            spawner_offset,
7015                            effect_batch.property_key,
7016                        );
7017
7018                        compute_pass.dispatch_workgroups_indirect(
7019                            event_cache.init_indirect_dispatch_buffer().unwrap(),
7020                            indirect_offset,
7021                        );
7022                    }
7023
7024                    // Direct dispatch via CPU spawn count
7025                    BatchSpawnInfo::CpuSpawner {
7026                        total_spawn_count: spawn_count,
7027                    } => {
7028                        assert!(!effect_batch
7029                            .layout_flags
7030                            .contains(LayoutFlags::CONSUME_GPU_SPAWN_EVENTS));
7031
7032                        const WORKGROUP_SIZE: u32 = 64;
7033                        let workgroup_count = spawn_count.div_ceil(WORKGROUP_SIZE);
7034
7035                        trace!(
7036                            "record commands for init pipeline of effect {:?} \
7037                                (spawn {} particles => {} workgroups) spawner_base={} \
7038                                spawner_offset={} \
7039                                property_key={:?}...",
7040                            effect_batch.handle,
7041                            spawn_count,
7042                            workgroup_count,
7043                            spawner_base,
7044                            spawner_offset,
7045                            effect_batch.property_key,
7046                        );
7047
7048                        compute_pass.dispatch_workgroups(workgroup_count, 1, 1);
7049                    }
7050                }
7051
7052                trace!("init compute dispatched");
7053            }
7054        }
7055
7056        // Compute indirect dispatch pass
7057        if let (
7058            Some(_),
7059            true,
7060            Some(indirect_metadata_bind_group),
7061            Some(indirect_sim_params_bind_group),
7062            Some(indirect_spawner_bind_group),
7063        ) = (
7064            effects_meta.spawner_buffer.buffer(),
7065            !effects_meta.spawner_buffer.is_empty(),
7066            &effects_meta.indirect_metadata_bind_group,
7067            &effects_meta.indirect_sim_params_bind_group,
7068            &effects_meta.indirect_spawner_bind_group,
7069        ) {
7070            // Only start a compute pass if there's an effect; makes things clearer in
7071            // debugger.
7072            let mut compute_pass =
7073                self.begin_compute_pass("hanabi:indirect_dispatch", pipeline_cache, render_context);
7074
7075            // Dispatch indirect dispatch compute job
7076            trace!("record commands for indirect dispatch pipeline...");
7077
7078            let has_gpu_spawn_events = !event_cache.child_infos().is_empty();
7079            if has_gpu_spawn_events {
7080                if let Some(indirect_child_info_buffer_bind_group) =
7081                    event_cache.indirect_child_info_buffer_bind_group()
7082                {
7083                    assert!(has_gpu_spawn_events);
7084                    compute_pass.set_bind_group(3, indirect_child_info_buffer_bind_group, &[]);
7085                } else {
7086                    error!("Missing child_info_buffer@3 bind group for the vfx_indirect pass.");
7087                    // render_context
7088                    //     .command_encoder()
7089                    //     .insert_debug_marker("ERROR:MissingIndirectBindGroup3");
7090                    // FIXME - Bevy doesn't allow returning custom errors here...
7091                    return Ok(());
7092                }
7093            }
7094
7095            if compute_pass
7096                .set_cached_compute_pipeline(effects_meta.active_indirect_pipeline_id)
7097                .is_err()
7098            {
7099                // FIXME - Bevy doesn't allow returning custom errors here...
7100                return Ok(());
7101            }
7102
7103            //error!("FIXME - effect_metadata_buffer has gaps!!!! this won't work. len() is
7104            // the size exluding gaps!");
7105            const WORKGROUP_SIZE: u32 = 64;
7106            //let total_effect_count = effects_meta.effect_metadata_buffer.len();
7107            let total_effect_count = effects_meta.spawner_buffer.len() as u32;
7108            let workgroup_count = total_effect_count.div_ceil(WORKGROUP_SIZE);
7109
7110            // Setup vfx_indirect pass
7111            compute_pass.set_bind_group(0, indirect_sim_params_bind_group, &[]);
7112            compute_pass.set_bind_group(1, indirect_metadata_bind_group, &[]);
7113            compute_pass.set_bind_group(2, indirect_spawner_bind_group, &[]);
7114            compute_pass.dispatch_workgroups(workgroup_count, 1, 1);
7115            trace!(
7116                "indirect dispatch compute dispatched: total_effect_count={} workgroup_count={}",
7117                total_effect_count,
7118                workgroup_count
7119            );
7120        }
7121
7122        // Compute update pass
7123        {
7124            let Some(indirect_buffer) = effects_meta.dispatch_indirect_buffer.buffer() else {
7125                warn!("Missing indirect buffer for update pass, cannot dispatch anything.");
7126                render_context
7127                    .command_encoder()
7128                    .insert_debug_marker("ERROR:MissingUpdateIndirectBuffer");
7129                // FIXME - Bevy doesn't allow returning custom errors here...
7130                return Ok(());
7131            };
7132
7133            let mut compute_pass =
7134                self.begin_compute_pass("hanabi:update", pipeline_cache, render_context);
7135
7136            // Bind group simparams@0 is common to everything, only set once per update pass
7137            compute_pass.set_bind_group(
7138                0,
7139                effects_meta.update_sim_params_bind_group.as_ref().unwrap(),
7140                &[],
7141            );
7142
7143            // Dispatch update compute jobs
7144            for effect_batch in sorted_effect_batches.iter() {
7145                // Fetch bind group particle@1
7146                let Some(particle_bind_group) =
7147                    effect_cache.particle_sim_bind_group(&effect_batch.slab_id)
7148                else {
7149                    error!(
7150                        "Failed to find update particle@1 bind group for slab #{}",
7151                        effect_batch.slab_id.index()
7152                    );
7153                    compute_pass.insert_debug_marker("ERROR:MissingParticleSimBindGroup");
7154                    continue;
7155                };
7156
7157                // Fetch bind group metadata@3
7158                let Some(metadata_bind_group) = effect_bind_groups
7159                    .update_metadata_bind_groups
7160                    .get(&effect_batch.slab_id)
7161                else {
7162                    error!(
7163                        "Failed to find update metadata@3 bind group for slab #{}",
7164                        effect_batch.slab_id.index()
7165                    );
7166                    compute_pass.insert_debug_marker("ERROR:MissingMetadataBindGroup");
7167                    continue;
7168                };
7169
7170                // Fetch compute pipeline
7171                if let Err(err) = compute_pass
7172                    .set_cached_compute_pipeline(effect_batch.init_and_update_pipeline_ids.update)
7173                {
7174                    compute_pass.insert_debug_marker(&format!(
7175                        "ERROR:FailedToSetCachedUpdatePipeline:{:?}",
7176                        err
7177                    ));
7178                    continue;
7179                }
7180
7181                // Compute dynamic offsets
7182                let spawner_base = effect_batch.spawner_base;
7183                let spawner_aligned_size = effects_meta.spawner_buffer.aligned_size();
7184                assert!(spawner_aligned_size >= GpuSpawnerParams::min_size().get() as usize);
7185                let spawner_offset = spawner_base * spawner_aligned_size as u32;
7186                let property_offset = effect_batch.property_offset;
7187
7188                trace!(
7189                    "record commands for update pipeline of effect {:?} spawner_base={}",
7190                    effect_batch.handle,
7191                    spawner_base,
7192                );
7193
7194                // Setup update pass
7195                compute_pass.set_bind_group(1, particle_bind_group, &[]);
7196                let offsets = if let Some(property_offset) = property_offset {
7197                    vec![spawner_offset, property_offset]
7198                } else {
7199                    vec![spawner_offset]
7200                };
7201                compute_pass.set_bind_group(
7202                    2,
7203                    property_bind_groups
7204                        .get(effect_batch.property_key.as_ref())
7205                        .unwrap(),
7206                    &offsets[..],
7207                );
7208                compute_pass.set_bind_group(3, &metadata_bind_group.bind_group, &[]);
7209
7210                // Dispatch update job
7211                let dispatch_indirect_offset = effect_batch
7212                    .dispatch_buffer_indices
7213                    .update_dispatch_indirect_buffer_row_index
7214                    * 12;
7215                trace!(
7216                    "dispatch_workgroups_indirect: buffer={:?} offset={}B",
7217                    indirect_buffer,
7218                    dispatch_indirect_offset,
7219                );
7220                compute_pass
7221                    .dispatch_workgroups_indirect(indirect_buffer, dispatch_indirect_offset as u64);
7222
7223                trace!("update compute dispatched");
7224            }
7225        }
7226
7227        // Compute sort fill dispatch pass - Fill the indirect dispatch structs for any
7228        // batch of particles which needs sorting, based on the actual number of alive
7229        // particles in the batch after their update in the compute update pass. Since
7230        // particles may die during update, this may be different from the number of
7231        // particles updated.
7232        if let Some(queue_index) = sorted_effect_batches.dispatch_queue_index.as_ref() {
7233            gpu_buffer_operations.dispatch(
7234                *queue_index,
7235                render_context,
7236                utils_pipeline,
7237                Some("hanabi:sort_fill_dispatch"),
7238            );
7239        }
7240
7241        // Compute sort pass
7242        {
7243            let mut compute_pass =
7244                self.begin_compute_pass("hanabi:sort", pipeline_cache, render_context);
7245
7246            let effect_metadata_buffer = effects_meta.effect_metadata_buffer.buffer().unwrap();
7247            let indirect_buffer = sort_bind_groups.indirect_buffer().unwrap();
7248
7249            // Loop on batches and find those which need sorting
7250            for effect_batch in sorted_effect_batches.iter() {
7251                trace!("Processing effect batch for sorting...");
7252                if !effect_batch.layout_flags.contains(LayoutFlags::RIBBONS) {
7253                    continue;
7254                }
7255                assert!(effect_batch.particle_layout.contains(Attribute::RIBBON_ID));
7256                assert!(effect_batch.particle_layout.contains(Attribute::AGE)); // or is that optional?
7257
7258                let Some(effect_buffer) = effect_cache.get_slab(&effect_batch.slab_id) else {
7259                    warn!("Missing sort-fill effect buffer.");
7260                    // render_context
7261                    //     .command_encoder()
7262                    //     .insert_debug_marker("ERROR:MissingEffectBatchBuffer");
7263                    continue;
7264                };
7265
7266                let indirect_dispatch_index = *effect_batch
7267                    .sort_fill_indirect_dispatch_index
7268                    .as_ref()
7269                    .unwrap();
7270                let indirect_offset =
7271                    sort_bind_groups.get_indirect_dispatch_byte_offset(indirect_dispatch_index);
7272
7273                // Fill the sort buffer with the key-value pairs to sort
7274                {
7275                    compute_pass.push_debug_group("hanabi:sort_fill");
7276
7277                    // Fetch compute pipeline
7278                    let Some(pipeline_id) =
7279                        sort_bind_groups.get_sort_fill_pipeline_id(&effect_batch.particle_layout)
7280                    else {
7281                        warn!("Missing sort-fill pipeline.");
7282                        compute_pass.insert_debug_marker("ERROR:MissingSortFillPipeline");
7283                        continue;
7284                    };
7285                    if compute_pass
7286                        .set_cached_compute_pipeline(pipeline_id)
7287                        .is_err()
7288                    {
7289                        compute_pass.insert_debug_marker("ERROR:FailedToSetSortFillPipeline");
7290                        compute_pass.pop_debug_group();
7291                        // FIXME - Bevy doesn't allow returning custom errors here...
7292                        return Ok(());
7293                    }
7294
7295                    let spawner_base = effect_batch.spawner_base;
7296                    let spawner_aligned_size = effects_meta.spawner_buffer.aligned_size();
7297                    assert!(spawner_aligned_size >= GpuSpawnerParams::min_size().get() as usize);
7298                    let spawner_offset = spawner_base * spawner_aligned_size as u32;
7299
7300                    // Bind group sort_fill@0
7301                    let particle_buffer = effect_buffer.particle_buffer();
7302                    let indirect_index_buffer = effect_buffer.indirect_index_buffer();
7303                    let Some(bind_group) = sort_bind_groups.sort_fill_bind_group(
7304                        particle_buffer.id(),
7305                        indirect_index_buffer.id(),
7306                        effect_metadata_buffer.id(),
7307                    ) else {
7308                        warn!("Missing sort-fill bind group.");
7309                        compute_pass.insert_debug_marker("ERROR:MissingSortFillBindGroup");
7310                        continue;
7311                    };
7312                    let effect_metadata_offset = effects_meta
7313                        .gpu_limits
7314                        .effect_metadata_offset(effect_batch.metadata_table_id.0)
7315                        as u32;
7316                    compute_pass.set_bind_group(
7317                        0,
7318                        bind_group,
7319                        &[effect_metadata_offset, spawner_offset],
7320                    );
7321
7322                    compute_pass
7323                        .dispatch_workgroups_indirect(indirect_buffer, indirect_offset as u64);
7324                    trace!("Dispatched sort-fill with indirect offset +{indirect_offset}");
7325
7326                    compute_pass.pop_debug_group();
7327                }
7328
7329                // Do the actual sort
7330                {
7331                    compute_pass.push_debug_group("hanabi:sort");
7332
7333                    if compute_pass
7334                        .set_cached_compute_pipeline(sort_bind_groups.sort_pipeline_id())
7335                        .is_err()
7336                    {
7337                        compute_pass.insert_debug_marker("ERROR:FailedToSetSortPipeline");
7338                        compute_pass.pop_debug_group();
7339                        // FIXME - Bevy doesn't allow returning custom errors here...
7340                        return Ok(());
7341                    }
7342
7343                    let Some(bind_group) = sort_bind_groups.sort_bind_group() else {
7344                        warn!("Missing sort bind group.");
7345                        compute_pass.insert_debug_marker("ERROR:MissingSortBindGroup");
7346                        continue;
7347                    };
7348                    compute_pass.set_bind_group(0, bind_group, &[]);
7349                    compute_pass
7350                        .dispatch_workgroups_indirect(indirect_buffer, indirect_offset as u64);
7351                    trace!("Dispatched sort with indirect offset +{indirect_offset}");
7352
7353                    compute_pass.pop_debug_group();
7354                }
7355
7356                // Copy the sorted particle indices back into the indirect index buffer, where
7357                // the render pass will read them.
7358                {
7359                    compute_pass.push_debug_group("hanabi:copy_sorted_indices");
7360
7361                    // Fetch compute pipeline
7362                    let pipeline_id = sort_bind_groups.get_sort_copy_pipeline_id();
7363                    if compute_pass
7364                        .set_cached_compute_pipeline(pipeline_id)
7365                        .is_err()
7366                    {
7367                        compute_pass.insert_debug_marker("ERROR:FailedToSetSortCopyPipeline");
7368                        compute_pass.pop_debug_group();
7369                        // FIXME - Bevy doesn't allow returning custom errors here...
7370                        return Ok(());
7371                    }
7372
7373                    let spawner_base = effect_batch.spawner_base;
7374                    let spawner_aligned_size = effects_meta.spawner_buffer.aligned_size();
7375                    assert!(spawner_aligned_size >= GpuSpawnerParams::min_size().get() as usize);
7376                    let spawner_offset = spawner_base * spawner_aligned_size as u32;
7377
7378                    // Bind group sort_copy@0
7379                    let indirect_index_buffer = effect_buffer.indirect_index_buffer();
7380                    let Some(bind_group) = sort_bind_groups.sort_copy_bind_group(
7381                        indirect_index_buffer.id(),
7382                        effect_metadata_buffer.id(),
7383                    ) else {
7384                        warn!("Missing sort-copy bind group.");
7385                        compute_pass.insert_debug_marker("ERROR:MissingSortCopyBindGroup");
7386                        continue;
7387                    };
7388                    let effect_metadata_offset = effects_meta
7389                        .effect_metadata_buffer
7390                        .dynamic_offset(effect_batch.metadata_table_id);
7391                    compute_pass.set_bind_group(
7392                        0,
7393                        bind_group,
7394                        &[effect_metadata_offset, spawner_offset],
7395                    );
7396
7397                    compute_pass
7398                        .dispatch_workgroups_indirect(indirect_buffer, indirect_offset as u64);
7399                    trace!("Dispatched sort-copy with indirect offset +{indirect_offset}");
7400
7401                    compute_pass.pop_debug_group();
7402                }
7403            }
7404        }
7405
7406        Ok(())
7407    }
7408}
7409
7410impl From<LayoutFlags> for ParticleRenderAlphaMaskPipelineKey {
7411    fn from(layout_flags: LayoutFlags) -> Self {
7412        if layout_flags.contains(LayoutFlags::USE_ALPHA_MASK) {
7413            ParticleRenderAlphaMaskPipelineKey::AlphaMask
7414        } else if layout_flags.contains(LayoutFlags::OPAQUE) {
7415            ParticleRenderAlphaMaskPipelineKey::Opaque
7416        } else {
7417            ParticleRenderAlphaMaskPipelineKey::Blend
7418        }
7419    }
7420}
7421
7422#[cfg(test)]
7423mod tests {
7424    use super::*;
7425
7426    #[test]
7427    fn layout_flags() {
7428        let flags = LayoutFlags::default();
7429        assert_eq!(flags, LayoutFlags::NONE);
7430    }
7431
7432    #[cfg(feature = "gpu_tests")]
7433    #[test]
7434    fn gpu_limits() {
7435        use crate::test_utils::MockRenderer;
7436
7437        let renderer = MockRenderer::new();
7438        let device = renderer.device();
7439        let limits = GpuLimits::from_device(&device);
7440
7441        // assert!(limits.storage_buffer_align().get() >= 1);
7442        assert!(limits.effect_metadata_offset(256) >= 256 * GpuEffectMetadata::min_size().get());
7443    }
7444
7445    #[cfg(feature = "gpu_tests")]
7446    #[test]
7447    fn gpu_ops_ifda() {
7448        use crate::test_utils::MockRenderer;
7449
7450        let renderer = MockRenderer::new();
7451        let device = renderer.device();
7452        let render_queue = renderer.queue();
7453
7454        let mut world = World::new();
7455        world.insert_resource(device.clone());
7456        let mut buffer_ops = GpuBufferOperations::from_world(&mut world);
7457
7458        let src_buffer = device.create_buffer(&BufferDescriptor {
7459            label: None,
7460            size: 256,
7461            usage: BufferUsages::STORAGE,
7462            mapped_at_creation: false,
7463        });
7464        let dst_buffer = device.create_buffer(&BufferDescriptor {
7465            label: None,
7466            size: 256,
7467            usage: BufferUsages::STORAGE,
7468            mapped_at_creation: false,
7469        });
7470
7471        // Two consecutive ops can be merged. This includes having contiguous slices
7472        // both in source and destination.
7473        buffer_ops.begin_frame();
7474        {
7475            let mut q = InitFillDispatchQueue::default();
7476            q.enqueue(0, 0);
7477            assert_eq!(q.queue.len(), 1);
7478            q.enqueue(1, 1);
7479            // Ops are not batched yet
7480            assert_eq!(q.queue.len(), 2);
7481            // On submit, the ops get batched together
7482            q.submit(&src_buffer, &dst_buffer, &mut buffer_ops);
7483            assert_eq!(buffer_ops.args_buffer.len(), 1);
7484        }
7485        buffer_ops.end_frame(&device, &render_queue);
7486
7487        // Even if out of order, the init fill dispatch ops are batchable. Here the
7488        // offsets are enqueued inverted.
7489        buffer_ops.begin_frame();
7490        {
7491            let mut q = InitFillDispatchQueue::default();
7492            q.enqueue(1, 1);
7493            assert_eq!(q.queue.len(), 1);
7494            q.enqueue(0, 0);
7495            // Ops are not batched yet
7496            assert_eq!(q.queue.len(), 2);
7497            // On submit, the ops get batched together
7498            q.submit(&src_buffer, &dst_buffer, &mut buffer_ops);
7499            assert_eq!(buffer_ops.args_buffer.len(), 1);
7500        }
7501        buffer_ops.end_frame(&device, &render_queue);
7502
7503        // However, both the source and destination need to be contiguous at the same
7504        // time. Here they are mixed so we can't batch.
7505        buffer_ops.begin_frame();
7506        {
7507            let mut q = InitFillDispatchQueue::default();
7508            q.enqueue(0, 1);
7509            assert_eq!(q.queue.len(), 1);
7510            q.enqueue(1, 0);
7511            // Ops are not batched yet
7512            assert_eq!(q.queue.len(), 2);
7513            // On submit, the ops cannot get batched together
7514            q.submit(&src_buffer, &dst_buffer, &mut buffer_ops);
7515            assert_eq!(buffer_ops.args_buffer.len(), 2);
7516        }
7517        buffer_ops.end_frame(&device, &render_queue);
7518    }
7519}