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
103const INDIRECT_INDEX_SIZE: u32 = 12;
106
107fn calc_hash<H: Hash>(value: &H) -> u64 {
109 let mut hasher = DefaultHasher::default();
110 value.hash(&mut hasher);
111 hasher.finish()
112}
113
114#[derive(Debug, Clone)]
116pub(crate) struct BufferBindingSource {
117 buffer: Buffer,
118 offset: u32,
119 size: NonZeroU32,
120}
121
122impl BufferBindingSource {
123 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#[derive(Debug, Default, Clone, Copy, Resource)]
153pub(crate) struct SimParams {
154 time: f64,
157 delta_time: f32,
159
160 virtual_time: f64,
163 virtual_delta_time: f32,
165
166 real_time: f64,
169 real_delta_time: f32,
171}
172
173#[repr(C)]
176#[derive(Debug, Copy, Clone, Pod, Zeroable, ShaderType)]
177struct GpuSimParams {
178 delta_time: f32,
180 time: f32,
184 virtual_delta_time: f32,
186 virtual_time: f32,
190 real_delta_time: f32,
192 real_time: f32,
196 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#[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 #[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
293pub(crate) trait StorageType {
295 fn aligned_size(alignment: u32) -> NonZeroU64;
300
301 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 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#[repr(C)]
338#[derive(Debug, Default, Clone, Copy, Pod, Zeroable, ShaderType)]
339pub(crate) struct GpuSpawnerParams {
340 transform: GpuCompressedTransform,
345 inverse_transform: GpuCompressedTransform,
349 spawn: i32,
351 seed: u32,
353 render_pong: u32,
359 effect_metadata_index: u32,
361 draw_indirect_index: u32,
364 slab_offset: u32,
367 parent_slab_offset: u32,
371}
372
373#[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#[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#[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#[repr(C)]
460#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Pod, Zeroable, ShaderType)]
461pub struct GpuEffectMetadata {
462 pub capacity: u32,
466 pub alive_count: u32,
469 pub max_update: u32,
471 pub max_spawn: u32,
473 pub indirect_write_index: u32,
477
478 pub indirect_dispatch_index: u32,
483 pub indirect_draw_index: u32,
489 pub init_indirect_dispatch_index: u32,
493 pub local_child_index: u32,
499 pub global_child_index: u32,
501 pub base_child_index: u32,
504
505 pub particle_stride: u32,
507 pub sort_key_offset: u32,
509 pub sort_key2_offset: u32,
511
512 pub particle_counter: u32,
519}
520
521#[derive(Debug)]
523pub(super) struct InitFillDispatchItem {
524 pub global_child_index: u32,
526 pub dispatch_indirect_index: u32,
529}
530
531#[derive(Debug, Default, Resource)]
539pub(super) struct InitFillDispatchQueue {
540 queue: Vec<InitFillDispatchItem>,
541 submitted_queue_index: Option<u32>,
542}
543
544impl InitFillDispatchQueue {
545 #[inline]
547 pub fn clear(&mut self) {
548 self.queue.clear();
549 self.submitted_queue_index = None;
550 }
551
552 #[inline]
554 pub fn is_empty(&self) -> bool {
555 self.queue.is_empty()
556 }
557
558 #[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 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 self.queue
582 .sort_unstable_by_key(|item| item.global_child_index);
583
584 let mut fill_queue = GpuBufferOperationQueue::new();
585
586 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#[derive(Resource)]
676pub(crate) struct DispatchIndirectPipeline {
677 sim_params_bind_group_layout_desc: BindGroupLayoutDescriptor,
679 effect_metadata_bind_group_layout_desc: BindGroupLayoutDescriptor,
681 spawner_bind_group_layout_desc: BindGroupLayoutDescriptor,
683 child_infos_bind_group_layout_desc: BindGroupLayoutDescriptor,
685 indirect_shader_noevent: Handle<Shader>,
687 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 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 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 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 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 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 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 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 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 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
879pub(super) enum GpuBufferOperationType {
880 #[allow(dead_code)]
887 Zero,
888 #[allow(dead_code)]
893 Copy,
894 FillDispatchArgs,
900 #[allow(dead_code)]
906 FillDispatchArgsSelf,
907}
908
909#[repr(C)]
911#[derive(Debug, Copy, Clone, PartialEq, Eq, Pod, Zeroable, ShaderType)]
912pub(super) struct GpuBufferOperationArgs {
913 src_offset: u32,
915 src_stride: u32,
917 dst_offset: u32,
920 dst_stride: u32,
922 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
957pub struct GpuBufferOperationQueue {
964 args: Vec<GpuBufferOperationArgs>,
966 operation_queue: Vec<QueuedOperation>,
968}
969
970impl GpuBufferOperationQueue {
971 pub fn new() -> Self {
973 Self {
974 args: vec![],
975 operation_queue: vec![],
976 }
977 }
978
979 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#[derive(Resource)]
1024pub(super) struct GpuBufferOperations {
1025 args_buffer: AlignedBufferVec<GpuBufferOperationArgs>,
1027
1028 bind_groups: HashMap<QueuedOperationBindGroupKey, BindGroup>,
1030
1031 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 pub fn begin_frame(&mut self) {
1059 self.args_buffer.clear();
1060 self.bind_groups.clear(); self.queues.clear();
1062 }
1063
1064 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 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 self.args_buffer.write_buffer(device, render_queue);
1089 }
1090
1091 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 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 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 const WORKGROUP_SIZE: u32 = 64;
1232 let num_ops = 1u32; 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#[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 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 &[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 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 shader: Handle<Shader>,
1540 particle_layout_min_binding_size: NonZeroU32,
1542 parent_particle_layout_min_binding_size: Option<NonZeroU32>,
1546 flags: ParticleInitPipelineKeyFlags,
1548 particle_bind_group_layout_desc: BindGroupLayoutDescriptor,
1550 spawner_bind_group_layout_desc: BindGroupLayoutDescriptor,
1552 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 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 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 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 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 shader: Handle<Shader>,
1664 particle_layout: ParticleLayout,
1666 parent_particle_layout_min_binding_size: Option<NonZeroU32>,
1670 num_event_buffers: u32,
1672 particle_bind_group_layout_desc: BindGroupLayoutDescriptor,
1674 spawner_bind_group_layout_desc: BindGroupLayoutDescriptor,
1676 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 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 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 pub fn cache_material(&mut self, layout: &TextureLayout) {
1744 if layout.layout.is_empty() {
1745 return;
1746 }
1747
1748 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 pub fn get_material(&self, layout: &TextureLayout) -> Option<&BindGroupLayoutDescriptor> {
1791 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 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 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 shader: Handle<Shader>,
1851 particle_layout: ParticleLayout,
1853 mesh_layout: Option<MeshVertexBufferLayoutRef>,
1854 texture_layout: TextureLayout,
1856 local_space_simulation: bool,
1860 alpha_mask: ParticleRenderAlphaMaskPipelineKey,
1863 alpha_mode: AlphaMode,
1865 flipbook: bool,
1869 needs_uv: bool,
1872 needs_normal: bool,
1875 needs_particle_fragment: bool,
1879 ribbons: bool,
1882 #[cfg(all(feature = "2d", feature = "3d"))]
1886 pipeline_mode: PipelineMode,
1887 msaa_samples: u32,
1889 hdr: bool,
1891}
1892
1893#[derive(Clone, Copy, Default, Hash, PartialEq, Eq, Debug)]
1894pub(crate) enum ParticleRenderAlphaMaskPipelineKey {
1895 #[default]
1896 Blend,
1897 AlphaMask,
1900 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 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 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 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 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 shader_defs.push("USE_ALPHA_MASK".into())
2011 }
2012 ParticleRenderAlphaMaskPipelineKey::Opaque => {
2013 shader_defs.push("OPAQUE".into())
2015 }
2016 }
2017
2018 if key.flipbook {
2020 shader_defs.push("FLIPBOOK".into());
2021 }
2022
2023 if key.needs_uv {
2025 shader_defs.push("NEEDS_UV".into());
2026 }
2027
2028 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 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 depth_write_enabled: false, 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 depth_write_enabled: matches!(
2059 key.alpha_mask,
2060 ParticleRenderAlphaMaskPipelineKey::AlphaMask
2061 | ParticleRenderAlphaMaskPipelineKey::Opaque
2062 ),
2063 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#[derive(Debug, Clone, PartialEq, Component)]
2144#[require(CachedPipelines, CachedReadyState, CachedEffectMetadata)]
2145pub(crate) struct ExtractedEffect {
2146 pub handle: Handle<EffectAsset>,
2150 pub particle_layout: ParticleLayout,
2152 pub capacity: u32,
2154 pub layout_flags: LayoutFlags,
2156 pub texture_layout: TextureLayout,
2158 pub textures: Vec<Handle<Image>>,
2160 pub alpha_mode: AlphaMode,
2162 pub effect_shaders: EffectShader,
2164 pub simulation_condition: SimulationCondition,
2166}
2167
2168#[derive(Debug, Clone, PartialEq, Component)]
2174pub(crate) struct ExtractedSpawner {
2175 pub spawn_count: u32,
2180 pub prng_seed: u32,
2182 pub transform: GlobalTransform,
2184 pub is_visible: bool,
2186}
2187
2188#[derive(Debug, Default, Component)]
2193pub(crate) struct CachedEffectMetadata {
2194 pub table_id: BufferTableId,
2196 pub metadata: GpuEffectMetadata,
2198}
2199
2200#[derive(Debug, Clone, Copy, PartialEq, Eq, Component)]
2207#[relationship(relationship_target = ChildrenEffects)]
2208pub(crate) struct ChildEffectOf {
2209 pub parent: Entity,
2211}
2212
2213#[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#[derive(Debug, Component)]
2258pub(crate) struct ExtractedProperties {
2259 pub property_layout: PropertyLayout,
2261 pub property_data: Option<Vec<u8>>,
2269}
2270
2271#[derive(Default, Resource)]
2272pub(crate) struct EffectAssetEvents {
2273 pub images: Vec<AssetEvent<Image>>,
2274}
2275
2276pub(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#[derive(Debug, Default, Clone, Copy, Resource)]
2309pub struct DebugSettings {
2310 pub start_capture_this_frame: bool,
2325
2326 pub start_capture_on_new_effect: bool,
2334
2335 pub capture_frame_count: u32,
2347}
2348
2349#[derive(Debug, Default, Clone, Copy, Resource)]
2350pub(crate) struct RenderDebugSettings {
2351 is_capturing: bool,
2353 capture_start: Duration,
2355 captured_frames: u32,
2357}
2358
2359pub(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 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 !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
2419pub(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
2433pub(crate) fn extract_effects(
2438 mut commands: Commands,
2439 effects: Extract<Res<Assets<EffectAsset>>>,
2440 default_mesh: Extract<Res<DefaultMesh>>,
2441 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 mut q_extracted_effects: Query<(
2456 &mut ExtractedEffect,
2457 Option<&mut ExtractedSpawner>,
2458 Option<&ChildEffectOf>, 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 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 let Some(effect_shaders) = compiled_effect.get_configured_shaders() else {
2482 trace!("Effect {:?}: no configured shader, skipped.", main_entity);
2483 continue;
2484 };
2485
2486 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 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 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 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 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 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 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 let Some(child_effect_of) = maybe_child_of {
2615 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 let property_layout = asset.property_layout();
2632 if property_layout.is_empty() {
2633 cmd.remove::<ExtractedProperties>();
2634 } else {
2635 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 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 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
2707struct GpuLimits {
2709 storage_buffer_align: NonZeroU32,
2713
2714 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 pub fn storage_buffer_align(&self) -> NonZeroU32 {
2748 self.storage_buffer_align
2749 }
2750
2751 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#[derive(Resource)]
2765pub struct EffectsMeta {
2766 view_bind_group: Option<BindGroup>,
2769 update_sim_params_bind_group: Option<BindGroup>,
2772 indirect_sim_params_bind_group: Option<BindGroup>,
2776 indirect_metadata_bind_group: Option<BindGroup>,
2779 indirect_spawner_bind_group: Option<BindGroup>,
2781 sim_params_uniforms: UniformBuffer<GpuSimParams>,
2784 spawner_buffer: AlignedBufferVec<GpuSpawnerParams>,
2787 dispatch_indirect_buffer: GpuBuffer<GpuDispatchIndirectArgs>,
2790 draw_indirect_buffer: BufferTable<GpuDrawIndexedIndirectArgs>,
2796 effect_metadata_buffer: BufferTable<GpuEffectMetadata>,
2799 gpu_limits: GpuLimits,
2802 indirect_shader_noevent: Handle<Shader>,
2803 indirect_shader_events: Handle<Shader>,
2804 indirect_pipeline_ids: [CachedComputePipelineId; 2],
2807 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 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 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 #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
2928 pub struct LayoutFlags: u32 {
2929 const NONE = 0;
2931 const LOCAL_SPACE_SIMULATION = (1 << 2);
2935 const USE_ALPHA_MASK = (1 << 3);
2937 const FLIPBOOK = (1 << 4);
2940 const NEEDS_UV = (1 << 5);
2942 const RIBBONS = (1 << 6);
2944 const NEEDS_NORMAL = (1 << 7);
2946 const OPAQUE = (1 << 8);
2948 const EMIT_GPU_SPAWN_EVENTS = (1 << 9);
2950 const CONSUME_GPU_SPAWN_EVENTS = (1 << 10);
2953 const READ_PARENT_PARTICLE = (1 << 11);
2957 const NEEDS_PARTICLE_FRAGMENT = (1 << 12);
2959 }
2960}
2961
2962impl Default for LayoutFlags {
2963 fn default() -> Self {
2964 Self::NONE
2965 }
2966}
2967
2968pub(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 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 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 effect_bind_groups.init_metadata_bind_groups.clear();
3017 effect_bind_groups.update_metadata_bind_groups.clear();
3018 }
3019 }
3020 }
3021 }
3022
3023 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 return;
3034 };
3035
3036 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
3049pub(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
3068pub(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
3083pub(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 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
3115pub 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 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
3164pub 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 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 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 {
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 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
3239pub 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 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 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 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 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 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 if let Some(mut cached_draw_indirect) = maybe_cached_draw_indirect_args {
3343 assert!(cached_draw_indirect.row.is_valid());
3344
3345 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 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
3375pub 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 }
3404 }
3405}
3406
3407pub fn allocate_parent_child_infos(
3410 mut commands: Commands,
3411 mut effect_cache: ResMut<EffectCache>,
3412 mut event_cache: ResMut<EventCache>,
3413 mut q_child_effects: Query<(
3416 Entity,
3417 &ExtractedEffect,
3418 &ChildEffectOf,
3419 &CachedEffectEvents,
3420 Option<&mut CachedChildInfo>,
3421 )>,
3422 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 for (child_entity, _, child_effect_of, cached_effect_events, maybe_cached_child_info) in
3438 &mut q_child_effects
3439 {
3440 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 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 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, 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 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 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 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 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 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 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 if let Some(mut cached_parent_info) = maybe_cached_parent_info {
3557 if cached_parent_info.children != new_children {
3558 event_cache.reallocate_child_infos(
3563 parent_entity,
3564 new_children,
3565 &new_child_infos[..],
3566 cached_parent_info.as_mut(),
3567 );
3568 }
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
3578pub 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 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 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 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 let init_pipeline_id = if let Some(init_pipeline_id) = cached_pipelines.init.as_ref() {
3672 *init_pipeline_id
3673 } else {
3674 cached_pipelines
3676 .flags
3677 .remove(CachedPipelineFlags::INIT_PIPELINE_READY);
3678
3679 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 let update_pipeline_id = if let Some(update_pipeline_id) = cached_pipelines.update.as_ref()
3723 {
3724 *update_pipeline_id
3725 } else {
3726 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 effect_cache.ensure_metadata_update_bind_group_layout_desc(num_event_buffers);
3742
3743 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 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 cached_pipelines
3789 .flags
3790 .insert(CachedPipelineFlags::INIT_PIPELINE_READY);
3791 trace!("[Effect {:?}] Init pipeline ready.", entity);
3792
3793 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 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 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 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 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
3871pub 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Component)]
3887pub(crate) struct ExtractedEffectMesh {
3888 pub mesh: AssetId<Mesh>,
3890}
3891
3892#[derive(Debug, Clone)]
3894#[allow(dead_code)]
3895pub(crate) struct MeshIndexSlice {
3896 pub format: IndexFormat,
3898 pub buffer: Buffer,
3900 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#[derive(Debug, Clone, PartialEq, Eq, Component)]
3919pub(crate) struct CachedMeshLocation {
3920 pub vertex_buffer: BufferId,
3922 pub vertex_or_index_count: u32,
3924 pub first_index_or_vertex_offset: u32,
3926 pub vertex_offset_or_base_instance: i32,
3928 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 const INIT_PIPELINE_READY = (1u8 << 0);
3938 const UPDATE_PIPELINE_READY = (1u8 << 1);
3940 }
3941}
3942
3943impl Default for CachedPipelineFlags {
3944 fn default() -> Self {
3945 Self::NONE
3946 }
3947}
3948
3949#[derive(Debug, Default, Component)]
3960pub(crate) struct CachedPipelines {
3961 pub flags: CachedPipelineFlags,
3963 pub init: Option<CachedComputePipelineId>,
3967 pub update: Option<CachedComputePipelineId>,
3971}
3972
3973impl CachedPipelines {
3974 #[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#[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
4028pub(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 orphaned_entities.clear();
4059 orphaned_entities.extend(orphaned.read());
4060 orphaned_entities.sort_unstable();
4061
4062 q_root_effects.par_iter_mut().for_each(
4066 |(entity, maybe_children, cached_pipelines, mut cached_ready_state)| {
4067 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 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 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 #[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 let (cached_ready_state, maybe_children) = {
4140 let Ok((cached_pipelines, mut cached_ready_state, maybe_children)) =
4141 (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 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 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 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
4191pub(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 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 effects_meta.spawner_buffer.clear();
4229
4230 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 if !cached_ready_state.is_ready() {
4254 trace!("Pipelines not ready for effect {}, skipped.", render_entity);
4255 continue;
4256 }
4257
4258 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 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 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 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 if extracted_effect.layout_flags.contains(LayoutFlags::RIBBONS) {
4305 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 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 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 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 {
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 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 effect_bind_groups.particle_slabs.clear();
4408 property_bind_groups.clear(true);
4409 effects_meta.indirect_spawner_bind_group = None;
4410 }
4411}
4412
4413pub(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 &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 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 sort_bind_groups.clear_indirect_dispatch_buffer();
4467
4468 let mut sort_queue = GpuBufferOperationQueue::new();
4469
4470 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 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 extracted_effect.layout_flags.contains(LayoutFlags::RIBBONS) {
4523 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 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 {
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; 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, );
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 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, );
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 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
4620pub(crate) struct BufferBindGroups {
4626 render: BindGroup,
4634 }
4656
4657#[derive(Debug, Default, Clone, PartialEq, Eq, Hash)]
4659struct Material {
4660 layout: TextureLayout,
4661 textures: Vec<AssetId<Image>>,
4662}
4663
4664impl Material {
4665 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
4770struct CachedBindGroup<K: Eq> {
4778 key: K,
4781 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
4822struct ConsumeEventBuffers<'a> {
4825 child_infos_buffer: &'a Buffer,
4828 events: BufferSlice<'a>,
4830}
4831
4832#[derive(Default, Resource)]
4833pub struct EffectBindGroups {
4834 particle_slabs: HashMap<SlabId, BufferBindGroups>,
4837 images: HashMap<AssetId<Image>, BindGroup>,
4839 init_metadata_bind_groups: HashMap<SlabId, CachedBindGroup<InitMetadataBindGroupKey>>,
4843 update_metadata_bind_groups: HashMap<SlabId, CachedBindGroup<UpdateMetadataBindGroupKey>>,
4847 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 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 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 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 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 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 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 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 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 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 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 (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 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 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 #[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 render_pipeline.cache_material(&effect_batch.texture_layout);
5174
5175 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 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 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 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 (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 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 #[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 render_pipeline.cache_material(&effect_batch.texture_layout);
5362
5363 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 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 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 let next_change_tick = change_tick.get() + 1;
5485 change_tick.set(next_change_tick);
5486
5487 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 return;
5509 }
5510
5511 #[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 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, extra_index: PhaseItemExtraIndex::None,
5544 indexed: true, },
5546 #[cfg(feature = "3d")]
5547 PipelineMode::Camera2d,
5548 );
5549 }
5550 }
5551
5552 #[cfg(feature = "3d")]
5554 {
5555 #[cfg(feature = "trace")]
5556 let _span_draw = bevy::log::info_span!("draw_3d").entered();
5557
5558 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, },
5587 #[cfg(feature = "2d")]
5588 PipelineMode::Camera3d,
5589 );
5590 }
5591
5592 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 || 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 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 || 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
5677pub 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 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
5720pub(crate) fn prepare_gpu_resources(
5726 mut effects_meta: ResMut<EffectsMeta>,
5727 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 let Some(view_binding) = view_uniforms.uniforms.binding() else {
5741 return;
5742 };
5743
5744 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 effects_meta.update_sim_params_bind_group = None;
5752 effects_meta.indirect_sim_params_bind_group = None;
5753 }
5754
5755 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 if effects_meta
5774 .draw_indirect_buffer
5775 .allocate_gpu(&render_device, &render_queue)
5776 {
5777 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 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 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
5798pub(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 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 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 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 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 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 if effects_meta
5955 .effect_metadata_buffer
5956 .allocate_gpu(render_device.as_ref(), render_queue.as_ref())
5957 {
5958 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
5966pub(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 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 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 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 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 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 let Some(spawner_buffer) = effects_meta.spawner_buffer.buffer().cloned() else {
6053 return;
6054 };
6055
6056 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 BindGroupEntry {
6066 binding: 0,
6067 resource: effects_meta.sim_params_uniforms.binding().unwrap(),
6068 },
6069 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), &[
6086 BindGroupEntry {
6088 binding: 0,
6089 resource: effects_meta.sim_params_uniforms.binding().unwrap(),
6090 },
6091 ],
6092 ));
6093 }
6094
6095 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 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 BindGroupEntry {
6117 binding: 0,
6118 resource: effect_metadata_buffer.as_entire_binding(),
6119 },
6120 BindGroupEntry {
6123 binding: 1,
6124 resource: dispatch_indirect_buffer.as_entire_binding(),
6125 },
6126 BindGroupEntry {
6129 binding: 2,
6130 resource: draw_indirect_buffer.as_entire_binding(),
6131 },
6132 ],
6133 ))
6134 }
6135
6136 _ => None,
6138 };
6139
6140 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 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 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 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 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 BindGroupEntry {
6195 binding: 0,
6196 resource: particle_slab.as_entire_binding_particle(),
6197 },
6198 BindGroupEntry {
6200 binding: 1,
6201 resource: particle_slab.as_entire_binding_indirect(),
6202 },
6203 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 gpu_buffer_operation_queue.create_bind_groups(&render_device, utils_pipeline);
6225
6226 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 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 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 {
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 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 {
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 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 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 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 if !effect_batch.texture_layout.layout.is_empty() {
6395 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 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 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
6452pub(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
6468fn 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 pass.set_vertex_buffer(0, vertex_buffer_slice.buffer.slice(..));
6518
6519 pass.set_bind_group(
6521 0,
6522 effects_meta.view_bind_group.as_ref().unwrap(),
6523 &[view_uniform.offset],
6524 );
6525
6526 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 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 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
6688pub(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: &'a PipelineCache,
6745 compute_pass: ComputePass<'a>,
6747 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
6802pub(crate) struct VfxSimulateNode {}
6807
6808impl VfxSimulateNode {
6809 pub fn new(_world: &mut World) -> Self {
6811 Self {}
6812 }
6813
6814 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 {
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 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 sorted_effect_batches.is_empty() {
6893 return Ok(());
6894 }
6895
6896 {
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 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 for effect_batch in sorted_effect_batches.iter() {
6915 {
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 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 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 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 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 match effect_batch.spawn_info {
6989 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 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 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 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 let mut compute_pass =
7073 self.begin_compute_pass("hanabi:indirect_dispatch", pipeline_cache, render_context);
7074
7075 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 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 return Ok(());
7101 }
7102
7103 const WORKGROUP_SIZE: u32 = 64;
7106 let total_effect_count = effects_meta.spawner_buffer.len() as u32;
7108 let workgroup_count = total_effect_count.div_ceil(WORKGROUP_SIZE);
7109
7110 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 {
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 return Ok(());
7131 };
7132
7133 let mut compute_pass =
7134 self.begin_compute_pass("hanabi:update", pipeline_cache, render_context);
7135
7136 compute_pass.set_bind_group(
7138 0,
7139 effects_meta.update_sim_params_bind_group.as_ref().unwrap(),
7140 &[],
7141 );
7142
7143 for effect_batch in sorted_effect_batches.iter() {
7145 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 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 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 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 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 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 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 {
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 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)); let Some(effect_buffer) = effect_cache.get_slab(&effect_batch.slab_id) else {
7259 warn!("Missing sort-fill effect buffer.");
7260 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 {
7275 compute_pass.push_debug_group("hanabi:sort_fill");
7276
7277 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 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 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 {
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 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 {
7359 compute_pass.push_debug_group("hanabi:copy_sorted_indices");
7360
7361 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 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 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.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 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 assert_eq!(q.queue.len(), 2);
7481 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 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 assert_eq!(q.queue.len(), 2);
7497 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 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 assert_eq!(q.queue.len(), 2);
7513 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}