use std::{
borrow::Cow,
hash::{DefaultHasher, Hash, Hasher},
marker::PhantomData,
num::{NonZeroU32, NonZeroU64},
ops::{Deref, DerefMut, Range},
time::Duration,
vec,
};
#[cfg(feature = "2d")]
use bevy::core_pipeline::core_2d::{Transparent2d, CORE_2D_DEPTH_FORMAT};
#[cfg(feature = "2d")]
use bevy::math::FloatOrd;
#[cfg(feature = "3d")]
use bevy::{
core_pipeline::{
core_3d::{
AlphaMask3d, Opaque3d, Opaque3dBatchSetKey, Opaque3dBinKey, Transparent3d,
CORE_3D_DEPTH_FORMAT,
},
prepass::{OpaqueNoLightmap3dBatchSetKey, OpaqueNoLightmap3dBinKey},
},
render::{
mesh::allocator::MeshSlabs,
render_phase::{BinnedPhaseItem, ViewBinnedRenderPhases},
},
};
use bevy::{
ecs::{
change_detection::Tick,
prelude::*,
system::{lifetimeless::*, SystemParam, SystemState},
},
log::trace,
mesh::MeshVertexBufferLayoutRef,
platform::collections::HashMap,
prelude::*,
render::{
mesh::{allocator::MeshAllocator, RenderMesh, RenderMeshBufferInfo},
render_asset::RenderAssets,
render_phase::{
Draw, DrawError, DrawFunctions, PhaseItemExtraIndex, SortedPhaseItem,
TrackedRenderPass, ViewSortedRenderPhases,
},
render_resource::{
binding_types::{
storage_buffer, storage_buffer_read_only, storage_buffer_read_only_sized,
storage_buffer_sized, uniform_buffer,
},
*,
},
renderer::{RenderContext, RenderDevice, RenderGraph, RenderGraphSystems, RenderQueue},
sync_world::{MainEntity, RenderEntity, TemporaryRenderEntity},
texture::GpuImage,
view::{
ExtractedView, RenderVisibleEntities, ViewUniform, ViewUniformOffset, ViewUniforms,
},
Extract, MainWorld,
},
};
use bitflags::bitflags;
use bytemuck::{Pod, Zeroable};
use effect_cache::{CachedEffect, EffectSlice, SlabState};
use event::{CachedChildInfo, CachedEffectEvents, CachedParentInfo, GpuChildInfo};
use fixedbitset::FixedBitSet;
use gpu_buffer::GpuBuffer;
use naga_oil::compose::{Composer, NagaModuleDescriptor};
use crate::{
asset::{DefaultMesh, EffectAsset},
calc_func_id,
render::{
batch::{BatchInput, EffectDrawBatch, EffectSorter, InitAndUpdatePipelineIds},
effect_cache::{AnyDrawIndirectArgs, CachedDrawIndirectArgs, SlabId},
},
AlphaMode, Attribute, CompiledParticleEffect, EffectProperties, EffectShaders,
EffectSimulation, EffectSpawner, EffectVisibilityClass, ParticleLayout, PropertyLayout,
SimulationCondition, TextureLayout,
};
mod aligned_buffer_vec;
mod batch;
mod buffer_table;
mod effect_cache;
mod event;
mod gpu_buffer;
mod property;
mod shader_cache;
mod sort;
use aligned_buffer_vec::AlignedBufferVec;
use batch::BatchSpawnInfo;
pub(crate) use batch::SortedEffectBatches;
use buffer_table::{BufferTable, BufferTableId};
pub(crate) use effect_cache::EffectCache;
pub(crate) use event::{allocate_events, on_remove_cached_effect_events, EventCache};
pub(crate) use property::{
allocate_properties, on_remove_cached_properties, prepare_property_buffers, PropertyBindGroups,
PropertyCache,
};
use property::{CachedEffectProperties, PropertyBindGroupKey};
pub use shader_cache::ShaderCache;
pub(crate) use sort::SortBindGroups;
use self::batch::EffectBatch;
#[derive(Debug, Clone, Copy)]
pub struct HanabiRenderPlugin;
impl Plugin for HanabiRenderPlugin {
fn name(&self) -> &str {
"hanabi:render"
}
fn build(&self, app: &mut App) {
let Some(render_app) = app.get_sub_app_mut(bevy::render::RenderApp) else {
return;
};
render_app.add_systems(
RenderGraph,
simulate
.in_set(RenderGraphSystems::Render)
.before(bevy::core_pipeline::schedule::camera_driver),
);
}
}
#[repr(C)]
#[derive(Debug, Copy, Clone, Pod, Zeroable, ShaderType)]
struct GpuIndirectIndex {
pub ping: u32,
pub pong: u32,
pub dead: u32,
}
fn calc_hash<H: Hash>(value: &H) -> u64 {
let mut hasher = DefaultHasher::default();
value.hash(&mut hasher);
hasher.finish()
}
#[derive(Debug, Clone)]
pub(crate) struct BufferBindingSource {
buffer: Buffer,
offset: u32,
size: NonZeroU32,
}
impl BufferBindingSource {
pub fn as_binding(&self) -> BindingResource<'_> {
BindingResource::Buffer(BufferBinding {
buffer: &self.buffer,
offset: self.offset as u64 * 4,
size: Some(self.size.into()),
})
}
}
impl PartialEq for BufferBindingSource {
fn eq(&self, other: &Self) -> bool {
self.buffer.id() == other.buffer.id()
&& self.offset == other.offset
&& self.size == other.size
}
}
impl<'a> From<&'a BufferBindingSource> for BufferBinding<'a> {
fn from(value: &'a BufferBindingSource) -> Self {
BufferBinding {
buffer: &value.buffer,
offset: value.offset as u64,
size: Some(value.size.into()),
}
}
}
#[derive(Debug, Default, Clone, Copy, Resource)]
pub(crate) struct SimParams {
time: f64,
delta_time: f32,
virtual_time: f64,
virtual_delta_time: f32,
real_time: f64,
real_delta_time: f32,
}
#[repr(C)]
#[derive(Debug, Copy, Clone, Pod, Zeroable, ShaderType)]
struct GpuSimParams {
delta_time: f32,
time: f32,
virtual_delta_time: f32,
virtual_time: f32,
real_delta_time: f32,
real_time: f32,
num_effects: u32,
}
impl Default for GpuSimParams {
fn default() -> Self {
Self {
delta_time: 0.04,
time: 0.0,
virtual_delta_time: 0.04,
virtual_time: 0.0,
real_delta_time: 0.04,
real_time: 0.0,
num_effects: 0,
}
}
}
impl From<SimParams> for GpuSimParams {
#[inline]
fn from(src: SimParams) -> Self {
Self::from(&src)
}
}
impl From<&SimParams> for GpuSimParams {
fn from(src: &SimParams) -> Self {
Self {
delta_time: src.delta_time,
time: src.time as f32,
virtual_delta_time: src.virtual_delta_time,
virtual_time: src.virtual_time as f32,
real_delta_time: src.real_delta_time,
real_time: src.real_time as f32,
..default()
}
}
}
#[repr(C)]
#[repr(align(16))] #[derive(Debug, Default, Clone, Copy, Pod, Zeroable, ShaderType)]
pub(crate) struct GpuCompressedTransform {
pub x_row: [f32; 4],
pub y_row: [f32; 4],
pub z_row: [f32; 4],
}
impl From<Mat4> for GpuCompressedTransform {
fn from(value: Mat4) -> Self {
let tr = value.transpose();
#[cfg(test)]
crate::test_utils::assert_approx_eq!(tr.w_axis, Vec4::W);
Self {
x_row: tr.x_axis.to_array(),
y_row: tr.y_axis.to_array(),
z_row: tr.z_axis.to_array(),
}
}
}
impl From<&Mat4> for GpuCompressedTransform {
fn from(value: &Mat4) -> Self {
let tr = value.transpose();
#[cfg(test)]
crate::test_utils::assert_approx_eq!(tr.w_axis, Vec4::W);
Self {
x_row: tr.x_axis.to_array(),
y_row: tr.y_axis.to_array(),
z_row: tr.z_axis.to_array(),
}
}
}
impl GpuCompressedTransform {
#[allow(dead_code)]
pub fn translation(&self) -> Vec3 {
Vec3 {
x: self.x_row[3],
y: self.y_row[3],
z: self.z_row[3],
}
}
}
pub(crate) trait StorageType {
fn aligned_size(alignment: u32) -> NonZeroU64;
fn padding_code(alignment: u32) -> String;
}
impl<T: ShaderType> StorageType for T {
fn aligned_size(alignment: u32) -> NonZeroU64 {
NonZeroU64::new(T::min_size().get().next_multiple_of(alignment as u64)).unwrap()
}
fn padding_code(alignment: u32) -> String {
let aligned_size = T::aligned_size(alignment);
trace!(
"Aligning {} to {} bytes as device limits requires. Orignal size: {} bytes. Aligned size: {} bytes.",
std::any::type_name::<T>(),
alignment,
T::min_size().get(),
aligned_size
);
if T::min_size() != aligned_size {
let padding_size = aligned_size.get() - T::min_size().get();
assert!(padding_size % 4 == 0);
format!("padding: array<u32, {}>", padding_size / 4)
} else {
"".to_string()
}
}
}
#[repr(C)]
#[derive(Debug, Default, Clone, Copy, Pod, Zeroable, ShaderType)]
pub(crate) struct GpuSpawnerParams {
transform: GpuCompressedTransform,
inverse_transform: GpuCompressedTransform,
spawn: i32,
seed: u32,
render_pong: u32,
effect_metadata_index: u32,
draw_indirect_index: u32,
slab_offset: u32,
parent_slab_offset: u32,
_padding0: u32,
}
impl GpuSpawnerParams {
pub fn new(
global_transform: &GlobalTransform,
spawn_count: u32,
prng_seed: u32,
slab_offset: u32,
parent_slab_offset: Option<u32>,
effect_metadata_buffer_table_id: BufferTableId,
maybe_cached_draw_indirect_args: Option<&CachedDrawIndirectArgs>,
) -> Self {
let transform = global_transform.to_matrix().into();
let inverse_transform = Mat4::from(
global_transform.affine().inverse(),
)
.into();
Self {
transform,
inverse_transform,
spawn: spawn_count as i32,
seed: prng_seed,
effect_metadata_index: effect_metadata_buffer_table_id.0,
draw_indirect_index: maybe_cached_draw_indirect_args
.map(|cdia| cdia.get_row().0)
.unwrap_or_default(),
slab_offset,
parent_slab_offset: parent_slab_offset.unwrap_or(u32::MAX),
..default()
}
}
}
#[repr(C)]
#[derive(Debug, Clone, Copy, Pod, Zeroable, ShaderType)]
pub struct GpuDispatchIndirectArgs {
pub x: u32,
pub y: u32,
pub z: u32,
}
impl Default for GpuDispatchIndirectArgs {
fn default() -> Self {
Self { x: 0, y: 1, z: 1 }
}
}
#[repr(C)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Pod, Zeroable, ShaderType)]
pub struct GpuDrawIndirectArgs {
pub vertex_count: u32,
pub instance_count: u32,
pub first_vertex: u32,
pub first_instance: u32,
}
impl Default for GpuDrawIndirectArgs {
fn default() -> Self {
Self {
vertex_count: 0,
instance_count: 1,
first_vertex: 0,
first_instance: 0,
}
}
}
#[repr(C)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Pod, Zeroable, ShaderType)]
pub struct GpuDrawIndexedIndirectArgs {
pub index_count: u32,
pub instance_count: u32,
pub first_index: u32,
pub base_vertex: i32,
pub first_instance: u32,
}
impl Default for GpuDrawIndexedIndirectArgs {
fn default() -> Self {
Self {
index_count: 0,
instance_count: 1,
first_index: 0,
base_vertex: 0,
first_instance: 0,
}
}
}
#[repr(C)]
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Pod, Zeroable, ShaderType)]
pub struct GpuBatchInfo {
pub total_spawn_count: u32,
pub total_update_count: u32,
pub base_effect: u32,
pub base_particle: u32,
pub prefix_sum_offset: u32,
pub prefix_sum_count: u32,
}
#[repr(C)]
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Pod, Zeroable, ShaderType)]
pub struct GpuEffectMetadata {
pub capacity: u32,
pub alive_count: u32,
pub max_update: u32,
pub max_spawn: u32,
pub indirect_write_index: u32,
pub indirect_draw_index: u32,
pub init_indirect_dispatch_index: u32,
pub properties_array_index: u32,
pub local_child_index: u32,
pub global_child_index: u32,
pub base_child_index: u32,
pub particle_stride: u32,
pub sort_key_offset: u32,
pub sort_key2_offset: u32,
pub particle_counter: u32,
}
#[derive(Debug)]
pub(super) struct InitFillDispatchItem {
pub global_child_index: u32,
pub dispatch_indirect_index: u32,
}
#[derive(Debug, Default, Resource)]
pub(super) struct InitFillDispatchQueue {
queue: Vec<InitFillDispatchItem>,
submitted_queue_index: Option<u32>,
}
impl InitFillDispatchQueue {
#[inline]
pub fn clear(&mut self) {
self.queue.clear();
self.submitted_queue_index = None;
}
#[inline]
pub fn is_empty(&self) -> bool {
self.queue.is_empty()
}
#[inline]
pub fn enqueue(&mut self, global_child_index: u32, dispatch_indirect_index: u32) {
assert!(global_child_index != u32::MAX);
self.queue.push(InitFillDispatchItem {
global_child_index,
dispatch_indirect_index,
});
}
pub fn submit(
&mut self,
src_buffer: &Buffer,
dst_buffer: &Buffer,
gpu_buffer_operations: &mut GpuBufferOperations,
) {
if self.queue.is_empty() {
return;
}
self.queue
.sort_unstable_by_key(|item| item.global_child_index);
let mut fill_queue = GpuBufferOperationQueue::new();
assert!(
self.queue[0].global_child_index != u32::MAX,
"Global child index not initialized"
);
let mut src_start = self.queue[0].global_child_index;
let mut dst_start = self.queue[0].dispatch_indirect_index;
let mut src_end = src_start + 1;
let mut dst_end = dst_start + 1;
let src_stride = GpuChildInfo::min_size().get() as u32 / 4;
let dst_stride = GpuDispatchIndirectArgs::SHADER_SIZE.get() as u32 / 4;
for i in 1..self.queue.len() {
let InitFillDispatchItem {
global_child_index: src,
dispatch_indirect_index: dst,
} = self.queue[i];
if src != src_end || dst != dst_end {
let count = src_end - src_start;
debug_assert_eq!(count, dst_end - dst_start);
let args = GpuBufferOperationArgs {
src_offset: src_start * src_stride + 1,
src_stride,
dst_offset: dst_start * dst_stride,
dst_stride,
count,
};
trace!(
"enqueue_init_fill(): src:global_child_index={} dst:init_indirect_dispatch_index={} args={:?} src_buffer={:?} dst_buffer={:?}",
src_start,
dst_start,
args,
src_buffer.id(),
dst_buffer.id(),
);
fill_queue.enqueue(
GpuBufferOperationType::FillDispatchArgs,
args,
src_buffer.clone(),
0,
None,
dst_buffer.clone(),
0,
None,
);
src_start = src;
dst_start = dst;
}
src_end = src + 1;
dst_end = dst + 1;
}
if src_start != src_end || dst_start != dst_end {
let count = src_end - src_start;
debug_assert_eq!(count, dst_end - dst_start);
let args = GpuBufferOperationArgs {
src_offset: src_start * src_stride + 1,
src_stride,
dst_offset: dst_start * dst_stride,
dst_stride,
count,
};
trace!(
"IFDA::submit(): src:global_child_index={} dst:init_indirect_dispatch_index={} args={:?} src_buffer={:?} dst_buffer={:?}",
src_start,
dst_start,
args,
src_buffer.id(),
dst_buffer.id(),
);
fill_queue.enqueue(
GpuBufferOperationType::FillDispatchArgs,
args,
src_buffer.clone(),
0,
None,
dst_buffer.clone(),
0,
None,
);
}
debug_assert!(self.submitted_queue_index.is_none());
if !fill_queue.operation_queue.is_empty() {
self.submitted_queue_index = Some(gpu_buffer_operations.submit(fill_queue));
}
}
}
#[derive(Debug)]
pub(super) struct SortFillDispatchItem {
pub metadata_table_id: BufferTableId,
pub sort_fill_indirect_dispatch_index: u32,
}
#[derive(Debug, Default, Resource)]
pub(super) struct SortFillDispatchQueue {
queue: Vec<SortFillDispatchItem>,
}
impl SortFillDispatchQueue {
#[inline]
pub fn clear(&mut self) {
self.queue.clear();
}
#[inline]
pub fn enqueue(
&mut self,
metadata_table_id: BufferTableId,
sort_fill_indirect_dispatch_index: u32,
) {
self.queue.push(SortFillDispatchItem {
metadata_table_id,
sort_fill_indirect_dispatch_index,
});
}
pub fn submit(
&self,
effect_metadata_buffer: &BufferTable<GpuEffectMetadata>,
effect_metadata_aligned_size: NonZeroU32,
sort_bind_groups: &SortBindGroups,
gpu_buffer_operations: &mut GpuBufferOperations,
) -> Option<u32> {
if self.queue.is_empty() {
return None;
}
let Some(src_buffer) = effect_metadata_buffer.buffer() else {
error!("Failed to find effect metadata buffer. This is a bug.");
return None;
};
let Some(dst_buffer) = sort_bind_groups.indirect_buffer() else {
error!("Missing indirect dispatch buffer for sorting, cannot schedule particle sort for ribbons. This is a bug.");
return None;
};
let src_offset = std::mem::offset_of!(GpuEffectMetadata, alive_count) as u32 / 4;
debug_assert_eq!(
src_offset, 1,
"GpuEffectMetadata changed, update this assert."
);
let src_stride = effect_metadata_aligned_size.get() / 4;
let dst_stride = GpuDispatchIndirectArgs::SHADER_SIZE.get() as u32 / 4;
let mut fill_queue = GpuBufferOperationQueue::new();
for item in &self.queue {
let src_binding_offset = effect_metadata_buffer.dynamic_offset(item.metadata_table_id);
let dst_offset = sort_bind_groups
.get_indirect_dispatch_byte_offset(item.sort_fill_indirect_dispatch_index)
/ 4;
trace!(
"queue_sort_fill_dispatch(): src#{:?}@+{}B ({}B) -> dst#{:?}@+{}B (whole)",
src_buffer.id(),
src_binding_offset,
effect_metadata_aligned_size.get(),
dst_buffer.id(),
dst_offset * 4,
);
fill_queue.enqueue(
GpuBufferOperationType::FillDispatchArgs,
GpuBufferOperationArgs {
src_offset,
src_stride,
dst_offset,
dst_stride,
count: 1,
},
src_buffer.clone(),
src_binding_offset,
Some(effect_metadata_aligned_size),
dst_buffer.clone(),
0,
None,
);
}
if fill_queue.operation_queue.is_empty() {
None
} else {
Some(gpu_buffer_operations.submit(fill_queue))
}
}
}
#[derive(Resource)]
pub(crate) struct DispatchIndirectPipeline {
sim_params_bind_group_layout_desc: BindGroupLayoutDescriptor,
effect_metadata_bind_group_layout_desc: BindGroupLayoutDescriptor,
spawner_bind_group_layout_desc: BindGroupLayoutDescriptor,
child_infos_bind_group_layout_desc: BindGroupLayoutDescriptor,
indirect_shader_noevent: Handle<Shader>,
indirect_shader_events: Handle<Shader>,
}
impl FromWorld for DispatchIndirectPipeline {
fn from_world(world: &mut World) -> Self {
let render_device = world.get_resource::<RenderDevice>().unwrap();
let (indirect_shader_noevent, indirect_shader_events) = {
let effects_meta = world.get_resource::<EffectsMeta>().unwrap();
(
effects_meta.indirect_shader_noevent.clone(),
effects_meta.indirect_shader_events.clone(),
)
};
let storage_alignment = render_device.limits().min_storage_buffer_offset_alignment;
let spawner_min_binding_size = GpuSpawnerParams::aligned_size(storage_alignment);
trace!("GpuSimParams: min_size={}", GpuSimParams::min_size());
let sim_params_bind_group_layout_desc = BindGroupLayoutDescriptor::new(
"hanabi:bgl:dispatch_indirect:sim_params",
&BindGroupLayoutEntries::single(
ShaderStages::COMPUTE,
uniform_buffer::<GpuSimParams>(false),
),
);
trace!(
"GpuEffectMetadata: min_size={}",
GpuEffectMetadata::min_size()
);
let effect_metadata_bind_group_layout_desc = BindGroupLayoutDescriptor::new(
"hanabi:bgl:dispatch_indirect:effect_metadata@1",
&BindGroupLayoutEntries::sequential(
ShaderStages::COMPUTE,
(
storage_buffer::<GpuEffectMetadata>(false),
storage_buffer::<GpuIndirectIndex>(false),
storage_buffer::<GpuDrawIndexedIndirectArgs>(false),
),
),
);
trace!(
"GpuSpawnerParams: min_size={} spawner_min_binding_size={}",
GpuSpawnerParams::min_size(),
spawner_min_binding_size
);
let spawner_bind_group_layout_desc = BindGroupLayoutDescriptor::new(
"hanabi:bgl:dispatch_indirect:spawner@2",
&BindGroupLayoutEntries::sequential(
ShaderStages::COMPUTE,
(
storage_buffer_sized(false, Some(spawner_min_binding_size)),
storage_buffer::<u32>(false),
),
),
);
trace!("GpuChildInfo: min_size={}", GpuChildInfo::min_size());
let child_infos_bind_group_layout_desc = BindGroupLayoutDescriptor::new(
"hanabi:bgl:dispatch_indirect:child_infos@3",
&BindGroupLayoutEntries::single(
ShaderStages::COMPUTE,
storage_buffer::<GpuChildInfo>(false),
),
);
Self {
sim_params_bind_group_layout_desc,
effect_metadata_bind_group_layout_desc,
spawner_bind_group_layout_desc,
child_infos_bind_group_layout_desc,
indirect_shader_noevent,
indirect_shader_events,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub(crate) struct DispatchIndirectPipelineKey {
has_events: bool,
}
impl SpecializedComputePipeline for DispatchIndirectPipeline {
type Key = DispatchIndirectPipelineKey;
fn specialize(&self, key: Self::Key) -> ComputePipelineDescriptor {
trace!(
"Specializing indirect pipeline (has_events={})",
key.has_events
);
let mut shader_defs = Vec::with_capacity(2);
if key.has_events {
shader_defs.push("HAS_GPU_SPAWN_EVENTS".into());
}
let mut layout = Vec::with_capacity(4);
layout.push(self.sim_params_bind_group_layout_desc.clone());
layout.push(self.effect_metadata_bind_group_layout_desc.clone());
layout.push(self.spawner_bind_group_layout_desc.clone());
if key.has_events {
layout.push(self.child_infos_bind_group_layout_desc.clone());
}
let label = format!(
"hanabi:compute_pipeline:dispatch_indirect{}",
if key.has_events {
"_events"
} else {
"_noevent"
}
);
ComputePipelineDescriptor {
label: Some(label.into()),
layout,
immediate_size: 0,
shader: if key.has_events {
self.indirect_shader_events.clone()
} else {
self.indirect_shader_noevent.clone()
},
shader_defs,
entry_point: Some("main".into()),
zero_initialize_workgroup_memory: false,
}
}
}
#[derive(Resource)]
pub(crate) struct PrefixSumPipeline {
bind_group_layout_desc: BindGroupLayoutDescriptor,
compute_shader: Handle<Shader>,
}
impl FromWorld for PrefixSumPipeline {
fn from_world(world: &mut World) -> Self {
let compute_shader = {
let effects_meta = world.get_resource::<EffectsMeta>().unwrap();
effects_meta.prefix_sum_shader.clone()
};
let min_storage_buffer_offset_alignment = {
let render_device = world.get_resource::<RenderDevice>().unwrap();
render_device.limits().min_storage_buffer_offset_alignment
};
let bind_group_layout_desc = BindGroupLayoutDescriptor::new(
"hanabi:bgl:prefix_sum:@0",
&BindGroupLayoutEntries::sequential(
ShaderStages::COMPUTE,
(
storage_buffer_sized(
false,
Some(GpuBatchInfo::aligned_size(
min_storage_buffer_offset_alignment,
)),
),
storage_buffer::<u32>(false),
storage_buffer::<GpuDispatchIndirectArgs>(false),
),
),
);
Self {
bind_group_layout_desc,
compute_shader,
}
}
}
impl PrefixSumPipeline {
pub fn make_pipeline_desc(&self) -> ComputePipelineDescriptor {
trace!("Create prefix sum pipeline");
ComputePipelineDescriptor {
label: Some("hanabi:compute_pipeline:prefix_sum".into()),
layout: vec![self.bind_group_layout_desc.clone()],
immediate_size: 0,
shader: self.compute_shader.clone(),
shader_defs: vec![],
entry_point: Some("main".into()),
zero_initialize_workgroup_memory: false,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum GpuBufferOperationType {
#[allow(dead_code)]
Zero,
#[allow(dead_code)]
Copy,
FillDispatchArgs,
#[allow(dead_code)]
FillDispatchArgsSelf,
}
#[repr(C)]
#[derive(Debug, Copy, Clone, PartialEq, Eq, Pod, Zeroable, ShaderType)]
pub(super) struct GpuBufferOperationArgs {
src_offset: u32,
src_stride: u32,
dst_offset: u32,
dst_stride: u32,
count: u32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
struct QueuedOperationBindGroupKey {
src_buffer: BufferId,
src_binding_size: Option<NonZeroU32>,
dst_buffer: BufferId,
dst_binding_size: Option<NonZeroU32>,
}
#[derive(Debug, Clone)]
struct QueuedOperation {
op: GpuBufferOperationType,
args_index: u32,
src_buffer: Buffer,
src_binding_offset: u32,
src_binding_size: Option<NonZeroU32>,
dst_buffer: Buffer,
dst_binding_offset: u32,
dst_binding_size: Option<NonZeroU32>,
}
impl From<&QueuedOperation> for QueuedOperationBindGroupKey {
fn from(value: &QueuedOperation) -> Self {
Self {
src_buffer: value.src_buffer.id(),
src_binding_size: value.src_binding_size,
dst_buffer: value.dst_buffer.id(),
dst_binding_size: value.dst_binding_size,
}
}
}
pub struct GpuBufferOperationQueue {
args: Vec<GpuBufferOperationArgs>,
operation_queue: Vec<QueuedOperation>,
}
impl GpuBufferOperationQueue {
pub fn new() -> Self {
Self {
args: vec![],
operation_queue: vec![],
}
}
pub fn enqueue(
&mut self,
op: GpuBufferOperationType,
args: GpuBufferOperationArgs,
src_buffer: Buffer,
src_binding_offset: u32,
src_binding_size: Option<NonZeroU32>,
dst_buffer: Buffer,
dst_binding_offset: u32,
dst_binding_size: Option<NonZeroU32>,
) -> u32 {
trace!(
"Queue {:?} op: args={:?} src_buffer={:?} src_binding_offset={} src_binding_size={:?} dst_buffer={:?} dst_binding_offset={} dst_binding_size={:?}",
op,
args,
src_buffer,
src_binding_offset,
src_binding_size,
dst_buffer,
dst_binding_offset,
dst_binding_size,
);
let args_index = self.args.len() as u32;
self.args.push(args);
self.operation_queue.push(QueuedOperation {
op,
args_index,
src_buffer,
src_binding_offset,
src_binding_size,
dst_buffer,
dst_binding_offset,
dst_binding_size,
});
args_index
}
}
#[derive(Resource)]
pub(super) struct GpuBufferOperations {
args_buffer: AlignedBufferVec<GpuBufferOperationArgs>,
bind_groups: HashMap<QueuedOperationBindGroupKey, BindGroup>,
queues: Vec<Vec<QueuedOperation>>,
}
impl FromWorld for GpuBufferOperations {
fn from_world(world: &mut World) -> Self {
let render_device = world.get_resource::<RenderDevice>().unwrap();
let align = render_device.limits().min_uniform_buffer_offset_alignment;
Self::new(align)
}
}
impl GpuBufferOperations {
pub fn new(align: u32) -> Self {
let args_buffer = AlignedBufferVec::new(
BufferUsages::UNIFORM,
Some(NonZeroU64::new(align as u64).unwrap()),
Some("hanabi:buffer:gpu_operation_args".to_string()),
);
Self {
args_buffer,
bind_groups: default(),
queues: vec![],
}
}
pub fn begin_frame(&mut self) {
self.args_buffer.clear();
self.bind_groups.clear(); self.queues.clear();
}
pub fn submit(&mut self, mut queue: GpuBufferOperationQueue) -> u32 {
assert!(!queue.operation_queue.is_empty());
let queue_index = self.queues.len() as u32;
for qop in &mut queue.operation_queue {
qop.args_index = self.args_buffer.push(queue.args[qop.args_index as usize]) as u32;
}
self.queues.push(queue.operation_queue);
queue_index
}
pub fn end_frame(&mut self, device: &RenderDevice, render_queue: &RenderQueue) {
assert_eq!(
self.args_buffer.len(),
self.queues.iter().fold(0, |len, q| len + q.len())
);
self.args_buffer.write_buffer(device, render_queue);
}
pub fn create_bind_groups(
&mut self,
render_device: &RenderDevice,
utils_pipeline: &UtilsPipeline,
) {
trace!(
"Creating bind groups for {} operation queues...",
self.queues.len()
);
for queue in &self.queues {
for qop in queue {
#[cfg(debug_assertions)]
if matches!(qop.op, GpuBufferOperationType::FillDispatchArgs) {
if let Some(size) = qop.src_binding_size {
debug_assert!(
qop.src_binding_offset as u64 + size.get() as u64
<= qop.src_buffer.size(),
"FillDispatchArgs src binding [{}..{}] overruns source buffer #{:?} of size {}.",
qop.src_binding_offset,
qop.src_binding_offset as u64 + size.get() as u64,
qop.src_buffer.id(),
qop.src_buffer.size(),
);
}
}
let key: QueuedOperationBindGroupKey = qop.into();
self.bind_groups.entry(key).or_insert_with(|| {
let src_id: NonZeroU32 = qop.src_buffer.id().into();
let dst_id: NonZeroU32 = qop.dst_buffer.id().into();
let label = format!("hanabi:bg:util_{}_{}", src_id.get(), dst_id.get());
let use_dynamic_offset = matches!(qop.op, GpuBufferOperationType::FillDispatchArgs);
let bind_group_layout =
utils_pipeline.bind_group_layout(qop.op, use_dynamic_offset);
let (src_offset, dst_offset) = if use_dynamic_offset {
(0, 0)
} else {
(qop.src_binding_offset as u64, qop.dst_binding_offset as u64)
};
trace!(
"-> Creating new bind group '{}': src#{} (@+{}B:{:?}B) dst#{} (@+{}B:{:?}B)",
label,
src_id,
src_offset,
qop.src_binding_size,
dst_id,
dst_offset,
qop.dst_binding_size,
);
render_device.create_bind_group(
Some(&label[..]),
bind_group_layout,
&[
BindGroupEntry {
binding: 0,
resource: BindingResource::Buffer(BufferBinding {
buffer: self.args_buffer.buffer().unwrap(),
offset: 0,
size: Some(
NonZeroU64::new(self.args_buffer.aligned_size() as u64)
.unwrap(),
),
}),
},
BindGroupEntry {
binding: 1,
resource: BindingResource::Buffer(BufferBinding {
buffer: &qop.src_buffer,
offset: src_offset,
size: qop.src_binding_size.map(Into::into),
}),
},
BindGroupEntry {
binding: 2,
resource: BindingResource::Buffer(BufferBinding {
buffer: &qop.dst_buffer,
offset: dst_offset,
size: qop.dst_binding_size.map(Into::into),
}),
},
],
)
});
}
}
}
pub fn dispatch(
&self,
index: u32,
render_context: &mut RenderContext,
utils_pipeline: &UtilsPipeline,
compute_pass_label: Option<&str>,
) {
let queue = &self.queues[index as usize];
trace!(
"Recording GPU commands for queue #{} ({} ops)...",
index,
queue.len(),
);
if queue.is_empty() {
return;
}
let mut compute_pass =
render_context
.command_encoder()
.begin_compute_pass(&ComputePassDescriptor {
label: compute_pass_label,
timestamp_writes: None,
});
let mut prev_op = None;
for qop in queue {
trace!("qop={:?}", qop);
if Some(qop.op) != prev_op {
compute_pass.set_pipeline(utils_pipeline.get_pipeline(qop.op));
prev_op = Some(qop.op);
}
let key: QueuedOperationBindGroupKey = qop.into();
if let Some(bind_group) = self.bind_groups.get(&key) {
let args_offset = self.args_buffer.dynamic_offset(qop.args_index as usize);
let use_dynamic_offset = matches!(qop.op, GpuBufferOperationType::FillDispatchArgs);
let (src_offset, dst_offset) = if use_dynamic_offset {
(qop.src_binding_offset, qop.dst_binding_offset)
} else {
(0, 0)
};
compute_pass.set_bind_group(0, bind_group, &[args_offset, src_offset, dst_offset]);
trace!(
"set bind group with args_offset=+{}B src_offset=+{}B dst_offset=+{}B",
args_offset,
src_offset,
dst_offset
);
} else {
error!("GPU fill dispatch buffer operation bind group not found for buffers src#{:?} dst#{:?}", qop.src_buffer.id(), qop.dst_buffer.id());
continue;
}
const WORKGROUP_SIZE: u32 = 64;
let num_ops = 1u32; let workgroup_count = num_ops.div_ceil(WORKGROUP_SIZE);
compute_pass.dispatch_workgroups(workgroup_count, 1, 1);
trace!(
"-> fill dispatch compute dispatched: num_ops={} workgroup_count={}",
num_ops,
workgroup_count
);
}
}
}
#[derive(Resource)]
pub(crate) struct UtilsPipeline {
#[allow(dead_code)]
bind_group_layout: BindGroupLayout,
bind_group_layout_dyn: BindGroupLayout,
bind_group_layout_no_src: BindGroupLayout,
pipelines: [ComputePipeline; 4],
}
impl FromWorld for UtilsPipeline {
fn from_world(world: &mut World) -> Self {
let render_device = world.get_resource::<RenderDevice>().unwrap();
let bind_group_layout = render_device.create_bind_group_layout(
"hanabi:bgl:utils",
&BindGroupLayoutEntries::sequential(
ShaderStages::COMPUTE,
(
uniform_buffer::<GpuBufferOperationArgs>(false),
storage_buffer_read_only::<u32>(false),
storage_buffer::<u32>(false),
),
),
);
let pipeline_layout = render_device.create_pipeline_layout(&PipelineLayoutDescriptor {
label: Some("hanabi:pipeline_layout:utils"),
bind_group_layouts: &[Some(&bind_group_layout)],
immediate_size: 0,
});
let bind_group_layout_dyn = render_device.create_bind_group_layout(
"hanabi:bgl:utils_dyn",
&BindGroupLayoutEntries::sequential(
ShaderStages::COMPUTE,
(
uniform_buffer::<GpuBufferOperationArgs>(true),
storage_buffer_read_only::<u32>(true),
storage_buffer::<u32>(true),
),
),
);
let pipeline_layout_dyn = render_device.create_pipeline_layout(&PipelineLayoutDescriptor {
label: Some("hanabi:pipeline_layout:utils_dyn"),
bind_group_layouts: &[Some(&bind_group_layout_dyn)],
immediate_size: 0,
});
let bind_group_layout_no_src = render_device.create_bind_group_layout(
"hanabi:bind_group_layout:utils_no_src",
&[
BindGroupLayoutEntry {
binding: 0,
visibility: ShaderStages::COMPUTE,
ty: BindingType::Buffer {
ty: BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: Some(GpuBufferOperationArgs::min_size()),
},
count: None,
},
BindGroupLayoutEntry {
binding: 2,
visibility: ShaderStages::COMPUTE,
ty: BindingType::Buffer {
ty: BufferBindingType::Storage { read_only: false },
has_dynamic_offset: false,
min_binding_size: NonZeroU64::new(4),
},
count: None,
},
],
);
let pipeline_layout_no_src =
render_device.create_pipeline_layout(&PipelineLayoutDescriptor {
label: Some("hanabi:pipeline_layout:utils_no_src"),
bind_group_layouts: &[Some(&bind_group_layout_no_src)],
immediate_size: 0,
});
let shader_code = include_str!("vfx_utils.wgsl");
let shader_source = {
let mut composer = Composer::default();
let shader_defs = default();
match composer.make_naga_module(NagaModuleDescriptor {
source: shader_code,
file_path: "vfx_utils.wgsl",
shader_defs,
..Default::default()
}) {
Ok(naga_module) => ShaderSource::Naga(Cow::Owned(naga_module)),
Err(compose_error) => panic!(
"Failed to compose vfx_utils.wgsl, naga_oil returned: {}",
compose_error.emit_to_string(&composer)
),
}
};
debug!("Create utils shader module:\n{}", shader_code);
#[allow(unsafe_code)]
let shader_module = unsafe {
render_device.create_shader_module(ShaderModuleDescriptor {
label: Some("hanabi:shader:utils"),
source: shader_source,
})
};
trace!("Create vfx_utils pipelines...");
let zero_pipeline = render_device.create_compute_pipeline(&RawComputePipelineDescriptor {
label: Some("hanabi:compute_pipeline:zero_buffer"),
layout: Some(&pipeline_layout),
module: &shader_module,
entry_point: Some("zero_buffer"),
compilation_options: PipelineCompilationOptions {
constants: &[],
zero_initialize_workgroup_memory: false,
},
cache: None,
});
let copy_pipeline = render_device.create_compute_pipeline(&RawComputePipelineDescriptor {
label: Some("hanabi:compute_pipeline:copy_buffer"),
layout: Some(&pipeline_layout_dyn),
module: &shader_module,
entry_point: Some("copy_buffer"),
compilation_options: PipelineCompilationOptions {
constants: &[],
zero_initialize_workgroup_memory: false,
},
cache: None,
});
let fill_dispatch_args_pipeline =
render_device.create_compute_pipeline(&RawComputePipelineDescriptor {
label: Some("hanabi:compute_pipeline:fill_dispatch_args"),
layout: Some(&pipeline_layout_dyn),
module: &shader_module,
entry_point: Some("fill_dispatch_args"),
compilation_options: PipelineCompilationOptions {
constants: &[],
zero_initialize_workgroup_memory: false,
},
cache: None,
});
let fill_dispatch_args_self_pipeline =
render_device.create_compute_pipeline(&RawComputePipelineDescriptor {
label: Some("hanabi:compute_pipeline:fill_dispatch_args_self"),
layout: Some(&pipeline_layout_no_src),
module: &shader_module,
entry_point: Some("fill_dispatch_args_self"),
compilation_options: PipelineCompilationOptions {
constants: &[],
zero_initialize_workgroup_memory: false,
},
cache: None,
});
Self {
bind_group_layout,
bind_group_layout_dyn,
bind_group_layout_no_src,
pipelines: [
zero_pipeline,
copy_pipeline,
fill_dispatch_args_pipeline,
fill_dispatch_args_self_pipeline,
],
}
}
}
impl UtilsPipeline {
fn get_pipeline(&self, op: GpuBufferOperationType) -> &ComputePipeline {
match op {
GpuBufferOperationType::Zero => &self.pipelines[0],
GpuBufferOperationType::Copy => &self.pipelines[1],
GpuBufferOperationType::FillDispatchArgs => &self.pipelines[2],
GpuBufferOperationType::FillDispatchArgsSelf => &self.pipelines[3],
}
}
fn bind_group_layout(
&self,
op: GpuBufferOperationType,
with_dynamic_offsets: bool,
) -> &BindGroupLayout {
if op == GpuBufferOperationType::FillDispatchArgsSelf {
assert!(
!with_dynamic_offsets,
"FillDispatchArgsSelf op cannot use dynamic offset (not implemented)"
);
&self.bind_group_layout_no_src
} else if with_dynamic_offsets {
&self.bind_group_layout_dyn
} else {
&self.bind_group_layout
}
}
}
#[derive(Resource)]
pub(crate) struct ParticlesInitPipeline {
sim_params_layout_desc: BindGroupLayoutDescriptor,
}
impl Default for ParticlesInitPipeline {
fn default() -> Self {
let sim_params_layout_desc = BindGroupLayoutDescriptor::new(
"hanabi:bgl:vfx_init:sim_params@0",
&BindGroupLayoutEntries::single(
ShaderStages::COMPUTE,
uniform_buffer::<GpuSimParams>(false),
),
);
Self {
sim_params_layout_desc,
}
}
}
bitflags! {
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct ParticleInitPipelineKeyFlags: u8 {
const ATTRIBUTE_PREV = (1u8 << 1);
const ATTRIBUTE_NEXT = (1u8 << 2);
const CONSUME_GPU_SPAWN_EVENTS = (1u8 << 3);
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub(crate) struct ParticleInitPipelineKey {
shader: Handle<Shader>,
particle_layout_min_binding_size: NonZeroU32,
parent_particle_layout_min_binding_size: Option<NonZeroU32>,
flags: ParticleInitPipelineKeyFlags,
particle_bind_group_layout_desc: BindGroupLayoutDescriptor,
spawner_bind_group_layout_desc: BindGroupLayoutDescriptor,
metadata_bind_group_layout_desc: BindGroupLayoutDescriptor,
}
impl SpecializedComputePipeline for ParticlesInitPipeline {
type Key = ParticleInitPipelineKey;
fn specialize(&self, key: Self::Key) -> ComputePipelineDescriptor {
let hash = calc_hash(&key);
trace!("Specializing init pipeline {hash:016X} with key {key:?}");
let mut shader_defs = Vec::with_capacity(4);
if key
.flags
.contains(ParticleInitPipelineKeyFlags::ATTRIBUTE_PREV)
{
shader_defs.push("ATTRIBUTE_PREV".into());
}
if key
.flags
.contains(ParticleInitPipelineKeyFlags::ATTRIBUTE_NEXT)
{
shader_defs.push("ATTRIBUTE_NEXT".into());
}
let consume_gpu_spawn_events = key
.flags
.contains(ParticleInitPipelineKeyFlags::CONSUME_GPU_SPAWN_EVENTS);
if consume_gpu_spawn_events {
shader_defs.push("CONSUME_GPU_SPAWN_EVENTS".into());
}
if key.parent_particle_layout_min_binding_size.is_some() {
assert!(consume_gpu_spawn_events);
shader_defs.push("READ_PARENT_PARTICLE".into());
} else {
assert!(!consume_gpu_spawn_events);
}
let label = format!("hanabi:pipeline:init_{hash:016X}");
trace!(
"-> creating pipeline '{}' with shader defs:{}",
label,
shader_defs
.iter()
.fold(String::new(), |acc, x| acc + &format!(" {x:?}"))
);
ComputePipelineDescriptor {
label: Some(label.into()),
layout: vec![
self.sim_params_layout_desc.clone(),
key.particle_bind_group_layout_desc.clone(),
key.spawner_bind_group_layout_desc.clone(),
key.metadata_bind_group_layout_desc.clone(),
],
immediate_size: 0,
shader: key.shader,
shader_defs,
entry_point: Some("main".into()),
zero_initialize_workgroup_memory: false,
}
}
}
#[derive(Resource)]
pub(crate) struct ParticlesUpdatePipeline {
sim_params_layout_desc: BindGroupLayoutDescriptor,
}
impl Default for ParticlesUpdatePipeline {
fn default() -> Self {
let sim_params_layout_desc = BindGroupLayoutDescriptor::new(
"hanabi:bgl:vfx_update:sim_params@0",
&BindGroupLayoutEntries::sequential(
ShaderStages::COMPUTE,
(
uniform_buffer::<GpuSimParams>(false),
storage_buffer::<GpuDrawIndexedIndirectArgs>(false),
),
),
);
Self {
sim_params_layout_desc,
}
}
}
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
pub(crate) struct ParticleUpdatePipelineKey {
shader: Handle<Shader>,
particle_layout: ParticleLayout,
parent_particle_layout_min_binding_size: Option<NonZeroU32>,
num_event_buffers: u32,
particle_bind_group_layout_desc: BindGroupLayoutDescriptor,
spawner_bind_group_layout_desc: BindGroupLayoutDescriptor,
metadata_bind_group_layout_desc: BindGroupLayoutDescriptor,
}
impl SpecializedComputePipeline for ParticlesUpdatePipeline {
type Key = ParticleUpdatePipelineKey;
fn specialize(&self, key: Self::Key) -> ComputePipelineDescriptor {
let hash = calc_hash(&key);
trace!("Specializing update pipeline {hash:016X} with key {key:?}");
let mut shader_defs = Vec::with_capacity(6);
shader_defs.push("EM_MAX_SPAWN_ATOMIC".into());
shader_defs.push("CHILD_INFO_EVENT_COUNT_IS_ATOMIC".into());
if key.particle_layout.contains(Attribute::PREV) {
shader_defs.push("ATTRIBUTE_PREV".into());
}
if key.particle_layout.contains(Attribute::NEXT) {
shader_defs.push("ATTRIBUTE_NEXT".into());
}
if key.parent_particle_layout_min_binding_size.is_some() {
shader_defs.push("READ_PARENT_PARTICLE".into());
}
if key.num_event_buffers > 0 {
shader_defs.push("EMITS_GPU_SPAWN_EVENTS".into());
}
let hash = calc_func_id(&key);
let label = format!("hanabi:pipeline:update_{hash:016X}");
trace!(
"-> creating pipeline '{}' with shader defs:{}",
label,
shader_defs
.iter()
.fold(String::new(), |acc, x| acc + &format!(" {x:?}"))
);
ComputePipelineDescriptor {
label: Some(label.into()),
layout: vec![
self.sim_params_layout_desc.clone(),
key.particle_bind_group_layout_desc.clone(),
key.spawner_bind_group_layout_desc.clone(),
key.metadata_bind_group_layout_desc.clone(),
],
immediate_size: 0,
shader: key.shader,
shader_defs,
entry_point: Some("main".into()),
zero_initialize_workgroup_memory: false,
}
}
}
#[derive(Resource)]
pub(crate) struct ParticlesRenderPipeline {
view_layout_desc: BindGroupLayoutDescriptor,
material_layout_descs: HashMap<TextureLayout, BindGroupLayoutDescriptor>,
}
impl ParticlesRenderPipeline {
pub fn cache_material(&mut self, layout: &TextureLayout) {
if layout.layout.is_empty() {
return;
}
if self.material_layout_descs.contains_key(layout) {
return;
}
let mut entries = Vec::with_capacity(layout.layout.len() * 2);
let mut index = 0;
for _slot in &layout.layout {
entries.push(BindGroupLayoutEntry {
binding: index,
visibility: ShaderStages::FRAGMENT,
ty: BindingType::Texture {
multisampled: false,
sample_type: TextureSampleType::Float { filterable: true },
view_dimension: TextureViewDimension::D2,
},
count: None,
});
entries.push(BindGroupLayoutEntry {
binding: index + 1,
visibility: ShaderStages::FRAGMENT,
ty: BindingType::Sampler(SamplerBindingType::Filtering),
count: None,
});
index += 2;
}
debug!(
"Creating material bind group with {} entries [{:?}] for layout {:?}",
entries.len(),
entries,
layout
);
let material_bind_group_layout_desc =
BindGroupLayoutDescriptor::new("hanabi:material_layout_render", &entries[..]);
self.material_layout_descs
.insert(layout.clone(), material_bind_group_layout_desc);
}
pub fn get_material(&self, layout: &TextureLayout) -> Option<&BindGroupLayoutDescriptor> {
if layout.layout.is_empty() {
return None;
}
self.material_layout_descs.get(layout)
}
}
impl FromWorld for ParticlesRenderPipeline {
fn from_world(_world: &mut World) -> Self {
let view_layout_desc = BindGroupLayoutDescriptor::new(
"hanabi:bgl:render:view@0",
&BindGroupLayoutEntries::sequential(
ShaderStages::VERTEX_FRAGMENT,
(
uniform_buffer::<ViewUniform>(true),
uniform_buffer::<GpuSimParams>(false),
),
),
);
Self {
view_layout_desc,
material_layout_descs: default(),
}
}
}
#[cfg(all(feature = "2d", feature = "3d"))]
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
enum PipelineMode {
Camera2d,
Camera3d,
}
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
pub(crate) struct ParticleRenderPipelineKey {
shader: Handle<Shader>,
particle_layout: ParticleLayout,
mesh_layout: Option<MeshVertexBufferLayoutRef>,
texture_layout: TextureLayout,
local_space_simulation: bool,
alpha_mask: ParticleRenderAlphaMaskPipelineKey,
alpha_mode: AlphaMode,
flipbook: bool,
needs_uv: bool,
needs_normal: bool,
needs_particle_fragment: bool,
ribbons: bool,
#[cfg(all(feature = "2d", feature = "3d"))]
pipeline_mode: PipelineMode,
msaa_samples: u32,
target_format: TextureFormat,
spawner_bind_group_layout_desc: BindGroupLayoutDescriptor,
}
#[derive(Clone, Copy, Default, Hash, PartialEq, Eq, Debug)]
pub(crate) enum ParticleRenderAlphaMaskPipelineKey {
#[default]
Blend,
AlphaMask,
Opaque,
}
impl SpecializedRenderPipeline for ParticlesRenderPipeline {
type Key = ParticleRenderPipelineKey;
fn specialize(&self, key: Self::Key) -> RenderPipelineDescriptor {
trace!("Specializing render pipeline for key: {key:?}");
trace!("Creating layout for bind group particle@1 of render pass");
let particle_bind_group_layout_desc = BindGroupLayoutDescriptor::new(
"hanabi:bgl:render:particle@1",
&BindGroupLayoutEntries::sequential(
ShaderStages::VERTEX,
(
storage_buffer_read_only_sized(
false,
Some(key.particle_layout.min_binding_size()),
)
.visibility(ShaderStages::VERTEX_FRAGMENT),
storage_buffer_read_only::<GpuIndirectIndex>(false),
),
),
);
let mut layout = vec![
self.view_layout_desc.clone(),
particle_bind_group_layout_desc,
key.spawner_bind_group_layout_desc.clone(),
];
let mut shader_defs = vec![];
let vertex_buffer_layout = key.mesh_layout.as_ref().and_then(|mesh_layout| {
mesh_layout
.0
.get_layout(&[
Mesh::ATTRIBUTE_POSITION.at_shader_location(0),
Mesh::ATTRIBUTE_UV_0.at_shader_location(1),
Mesh::ATTRIBUTE_NORMAL.at_shader_location(2),
])
.ok()
});
if let Some(material_bind_group_layout) = self.get_material(&key.texture_layout) {
layout.push(material_bind_group_layout.clone());
}
if key.local_space_simulation {
shader_defs.push("LOCAL_SPACE_SIMULATION".into());
}
match key.alpha_mask {
ParticleRenderAlphaMaskPipelineKey::Blend => {}
ParticleRenderAlphaMaskPipelineKey::AlphaMask => {
shader_defs.push("USE_ALPHA_MASK".into())
}
ParticleRenderAlphaMaskPipelineKey::Opaque => {
shader_defs.push("OPAQUE".into())
}
}
if key.flipbook {
shader_defs.push("FLIPBOOK".into());
}
if key.needs_uv {
shader_defs.push("NEEDS_UV".into());
}
if key.needs_normal {
shader_defs.push("NEEDS_NORMAL".into());
}
if key.needs_particle_fragment {
shader_defs.push("NEEDS_PARTICLE_FRAGMENT".into());
}
if key.ribbons {
shader_defs.push("RIBBONS".into());
}
#[cfg(feature = "2d")]
let depth_stencil_2d = DepthStencilState {
format: CORE_2D_DEPTH_FORMAT,
depth_write_enabled: Some(false), depth_compare: Some(CompareFunction::GreaterEqual),
stencil: StencilState::default(),
bias: DepthBiasState::default(),
};
#[cfg(feature = "3d")]
let depth_stencil_3d = DepthStencilState {
format: CORE_3D_DEPTH_FORMAT,
depth_write_enabled: Some(matches!(
key.alpha_mask,
ParticleRenderAlphaMaskPipelineKey::AlphaMask
| ParticleRenderAlphaMaskPipelineKey::Opaque
)),
depth_compare: Some(CompareFunction::GreaterEqual),
stencil: StencilState::default(),
bias: DepthBiasState::default(),
};
#[cfg(all(feature = "2d", feature = "3d"))]
assert_eq!(CORE_2D_DEPTH_FORMAT, CORE_3D_DEPTH_FORMAT);
#[cfg(all(feature = "2d", feature = "3d"))]
let depth_stencil = match key.pipeline_mode {
PipelineMode::Camera2d => Some(depth_stencil_2d),
PipelineMode::Camera3d => Some(depth_stencil_3d),
};
#[cfg(all(feature = "2d", not(feature = "3d")))]
let depth_stencil = Some(depth_stencil_2d);
#[cfg(all(feature = "3d", not(feature = "2d")))]
let depth_stencil = Some(depth_stencil_3d);
let format = key.target_format;
let hash = calc_func_id(&key);
let label = format!("hanabi:pipeline:render_{hash:016X}");
trace!(
"-> creating pipeline '{}' with shader defs:{}",
label,
shader_defs
.iter()
.fold(String::new(), |acc, x| acc + &format!(" {x:?}"))
);
RenderPipelineDescriptor {
label: Some(label.into()),
layout,
immediate_size: 0,
vertex: VertexState {
shader: key.shader.clone(),
entry_point: Some("vertex".into()),
shader_defs: shader_defs.clone(),
buffers: vec![vertex_buffer_layout.expect("Vertex buffer layout not present")],
},
fragment: Some(FragmentState {
shader: key.shader,
shader_defs,
entry_point: Some("fragment".into()),
targets: vec![Some(ColorTargetState {
format,
blend: Some(key.alpha_mode.into()),
write_mask: ColorWrites::ALL,
})],
}),
primitive: PrimitiveState {
front_face: FrontFace::Ccw,
cull_mode: None,
unclipped_depth: false,
polygon_mode: PolygonMode::Fill,
conservative: false,
topology: PrimitiveTopology::TriangleList,
strip_index_format: None,
},
depth_stencil,
multisample: MultisampleState {
count: key.msaa_samples,
mask: !0,
alpha_to_coverage_enabled: false,
},
zero_initialize_workgroup_memory: false,
}
}
}
#[derive(Debug, Clone, PartialEq, Component)]
#[require(CachedPipelines, CachedReadyState, CachedEffectMetadata)]
pub(crate) struct ExtractedEffect {
pub handle: Handle<EffectAsset>,
pub particle_layout: ParticleLayout,
pub capacity: u32,
pub layout_flags: LayoutFlags,
pub texture_layout: TextureLayout,
pub textures: Vec<Handle<Image>>,
pub alpha_mode: AlphaMode,
pub effect_shaders: EffectShaders,
pub simulation_condition: SimulationCondition,
}
#[derive(Debug, Clone, PartialEq, Component)]
pub(crate) struct ExtractedSpawner {
pub spawn_count: u32,
pub prng_seed: u32,
pub transform: GlobalTransform,
pub is_visible: bool,
}
#[derive(Debug, Default, Component)]
pub(crate) struct CachedEffectMetadata {
pub table_id: BufferTableId,
pub metadata: GpuEffectMetadata,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Component)]
#[relationship(relationship_target = ChildrenEffects)]
pub(crate) struct ChildEffectOf {
pub parent: Entity,
}
#[derive(Debug, Clone, PartialEq, Eq, Component)]
#[relationship_target(relationship = ChildEffectOf)]
pub(crate) struct ChildrenEffects(Vec<Entity>);
impl<'a> IntoIterator for &'a ChildrenEffects {
type Item = <Self::IntoIter as Iterator>::Item;
type IntoIter = std::slice::Iter<'a, Entity>;
#[inline(always)]
fn into_iter(self) -> Self::IntoIter {
self.0.iter()
}
}
impl Deref for ChildrenEffects {
type Target = [Entity];
fn deref(&self) -> &Self::Target {
&self.0
}
}
#[derive(Debug, Component)]
pub(crate) struct ExtractedProperties {
pub property_layout: PropertyLayout,
pub property_data: Option<Vec<u8>>,
}
#[derive(Default, Resource)]
pub(crate) struct EffectAssetEvents {
pub images: Vec<AssetEvent<Image>>,
}
pub(crate) fn extract_effect_events(
mut events: ResMut<EffectAssetEvents>,
mut image_events: Extract<MessageReader<AssetEvent<Image>>>,
) {
#[cfg(feature = "trace")]
let _span = bevy::log::info_span!("extract_effect_events").entered();
trace!("extract_effect_events()");
let EffectAssetEvents { ref mut images } = *events;
*images = image_events.read().copied().collect();
}
#[derive(Debug, Default, Clone, Copy, Resource)]
pub struct DebugSettings {
pub start_capture_this_frame: bool,
pub start_capture_on_new_effect: bool,
pub capture_frame_count: u32,
}
#[derive(Debug, Default, Clone, Copy, Resource)]
pub(crate) struct RenderDebugSettings {
is_capturing: bool,
capture_start: Duration,
captured_frames: u32,
}
pub(crate) fn start_stop_gpu_debug_capture(
real_time: Extract<Res<Time<Real>>>,
render_device: Res<RenderDevice>,
debug_settings: Extract<Res<DebugSettings>>,
mut render_debug_settings: ResMut<RenderDebugSettings>,
q_added_effects: Extract<Query<(), Added<CompiledParticleEffect>>>,
) {
#[cfg(feature = "trace")]
let _span = bevy::log::info_span!("start_stop_debug_capture").entered();
trace!("start_stop_debug_capture()");
if render_debug_settings.is_capturing {
render_debug_settings.captured_frames += 1;
if render_debug_settings.captured_frames >= debug_settings.capture_frame_count {
#[expect(unsafe_code, reason = "Debugging only")]
unsafe {
render_device.wgpu_device().stop_graphics_debugger_capture();
}
render_debug_settings.is_capturing = false;
warn!(
"Stopped GPU debug capture after {} frames, at t={}s.",
render_debug_settings.captured_frames,
real_time.elapsed().as_secs_f64()
);
}
}
if !render_debug_settings.is_capturing
&& (debug_settings.start_capture_this_frame
|| (debug_settings.start_capture_on_new_effect && !q_added_effects.is_empty()))
{
#[expect(unsafe_code, reason = "Debugging only")]
unsafe {
render_device
.wgpu_device()
.start_graphics_debugger_capture();
}
render_debug_settings.is_capturing = true;
render_debug_settings.capture_start = real_time.elapsed();
render_debug_settings.captured_frames = 0;
warn!(
"Started GPU debug capture of {} frames at t={}s.",
debug_settings.capture_frame_count,
render_debug_settings.capture_start.as_secs_f64()
);
}
}
pub(crate) fn report_ready_state(
mut main_world: ResMut<MainWorld>,
q_ready_state: Query<&CachedReadyState>,
) {
let mut q_effects = main_world.query::<(RenderEntity, &mut CompiledParticleEffect)>();
for (render_entity, mut compiled_particle_effect) in q_effects.iter_mut(&mut main_world) {
if let Ok(cached_ready_state) = q_ready_state.get(render_entity) {
compiled_particle_effect.is_ready = cached_ready_state.is_ready();
}
}
}
pub(crate) fn extract_effects(
mut commands: Commands,
effects: Extract<Res<Assets<EffectAsset>>>,
default_mesh: Extract<Res<DefaultMesh>>,
q_effects: Extract<
Query<(
Entity,
RenderEntity,
Option<&InheritedVisibility>,
Option<&ViewVisibility>,
&EffectSpawner,
&CompiledParticleEffect,
Option<Ref<EffectProperties>>,
&GlobalTransform,
)>,
>,
mut q_extracted_effects: Query<(
&mut ExtractedEffect,
Option<&mut ExtractedSpawner>,
Option<&ChildEffectOf>, // immutable, because of relationship
Option<&mut ExtractedEffectMesh>,
Option<&mut ExtractedProperties>,
)>,
) {
#[cfg(feature = "trace")]
let _span = bevy::log::info_span!("extract_effects").entered();
trace!("extract_effects()");
trace!("Extracting {} effects...", q_effects.iter().len());
for (
main_entity,
render_entity,
maybe_inherited_visibility,
maybe_view_visibility,
effect_spawner,
compiled_effect,
maybe_properties,
transform,
) in q_effects.iter()
{
let Some(effect_shaders) = compiled_effect.get_configured_shaders() else {
trace!("Effect {:?}: no configured shader, skipped.", main_entity);
continue;
};
let Some(asset) = effects.get(&compiled_effect.asset) else {
trace!(
"Effect {:?}: EffectAsset not ready, skipped. asset:{:?}",
main_entity,
compiled_effect.asset
);
continue;
};
let is_visible = maybe_inherited_visibility
.map(|cv| cv.get())
.unwrap_or(true)
&& maybe_view_visibility.map(|cv| cv.get()).unwrap_or(true);
let mut cmd = commands.entity(render_entity);
let (
maybe_extracted_effect,
maybe_extracted_spawner,
maybe_child_of,
maybe_extracted_mesh,
maybe_extracted_properties,
) = q_extracted_effects
.get_mut(render_entity)
.map(|(extracted_effect, b, c, d, e)| (Some(extracted_effect), b, c, d, e))
.unwrap_or((None, None, None, None, None));
let texture_layout = asset.module().texture_layout();
let layout_flags = compiled_effect.layout_flags;
let alpha_mode = compiled_effect.alpha_mode;
trace!(
"Extracted instance of effect '{}' on entity {:?} (render entity {:?}): texture_layout_count={} texture_count={} layout_flags={:?}",
asset.name,
main_entity,
render_entity,
texture_layout.layout.len(),
compiled_effect.textures.len(),
layout_flags,
);
let new_extracted_effect = ExtractedEffect {
handle: compiled_effect.asset.clone(),
particle_layout: asset.particle_layout().clone(),
capacity: asset.capacity(),
layout_flags,
texture_layout,
textures: compiled_effect.textures.clone(),
alpha_mode,
effect_shaders: effect_shaders.clone(),
simulation_condition: asset.simulation_condition,
};
if let Some(mut extracted_effect) = maybe_extracted_effect {
extracted_effect.set_if_neq(new_extracted_effect);
} else {
trace!(
"Inserting new ExtractedEffect component on {:?}",
render_entity
);
cmd.insert(new_extracted_effect);
}
let new_spawner = ExtractedSpawner {
spawn_count: effect_spawner.spawn_count,
prng_seed: compiled_effect.prng_seed,
transform: *transform,
is_visible,
};
trace!(
"[Effect {}] spawn_count={} prng_seed={}",
render_entity,
new_spawner.spawn_count,
new_spawner.prng_seed
);
if let Some(mut extracted_spawner) = maybe_extracted_spawner {
extracted_spawner.set_if_neq(new_spawner);
} else {
trace!(
"Inserting new ExtractedSpawner component on {}",
render_entity
);
cmd.insert(new_spawner);
}
let mesh = compiled_effect
.mesh
.clone()
.unwrap_or(default_mesh.0.clone());
let new_mesh = ExtractedEffectMesh { mesh: mesh.id() };
if let Some(mut extracted_mesh) = maybe_extracted_mesh {
extracted_mesh.set_if_neq(new_mesh);
} else {
trace!(
"Inserting new ExtractedEffectMesh component on {:?}",
render_entity
);
cmd.insert(new_mesh);
}
let parent_render_entity = if let Some(main_entity) = compiled_effect.parent {
let Ok((_, render_entity, _, _, _, _, _, _)) = q_effects.get(main_entity) else {
error!(
"Failed to resolve render entity of parent with main entity {:?}.",
main_entity
);
cmd.remove::<ChildEffectOf>();
continue;
};
Some(render_entity)
} else {
None
};
if let Some(render_entity) = parent_render_entity {
let new_child_of = ChildEffectOf {
parent: render_entity,
};
if let Some(child_effect_of) = maybe_child_of {
if *child_effect_of != new_child_of {
cmd.insert(new_child_of);
}
} else {
trace!(
"Inserting new ChildEffectOf component on {:?}",
render_entity
);
cmd.insert(new_child_of);
}
} else {
cmd.remove::<ChildEffectOf>();
}
let property_layout = asset.property_layout();
if property_layout.is_empty() {
cmd.remove::<ExtractedProperties>();
} else {
let property_data = if let Some(properties) = maybe_properties {
if properties.is_changed() {
trace!("Detected property change, re-serializing...");
Some(properties.serialize(&property_layout))
} else {
None
}
} else {
None
};
let new_properties = ExtractedProperties {
property_layout,
property_data,
};
trace!("new_properties = {new_properties:?}");
if let Some(mut extracted_properties) = maybe_extracted_properties {
if new_properties.property_data.is_some()
|| (extracted_properties.property_layout != new_properties.property_layout)
{
trace!(
"Updating existing ExtractedProperties (was: {:?})",
extracted_properties.as_ref()
);
*extracted_properties = new_properties;
}
} else {
trace!(
"Inserting new ExtractedProperties component on {:?}",
render_entity
);
cmd.insert(new_properties);
}
}
}
}
pub(crate) fn extract_sim_params(
real_time: Extract<Res<Time<Real>>>,
virtual_time: Extract<Res<Time<Virtual>>>,
time: Extract<Res<Time<EffectSimulation>>>,
mut sim_params: ResMut<SimParams>,
) {
#[cfg(feature = "trace")]
let _span = bevy::log::info_span!("extract_sim_params").entered();
trace!("extract_sim_params()");
sim_params.time = time.elapsed_secs_f64();
sim_params.delta_time = time.delta_secs();
sim_params.virtual_time = virtual_time.elapsed_secs_f64();
sim_params.virtual_delta_time = virtual_time.delta_secs();
sim_params.real_time = real_time.elapsed_secs_f64();
sim_params.real_delta_time = real_time.delta_secs();
trace!(
"SimParams: time={} delta_time={} vtime={} delta_vtime={} rtime={} delta_rtime={}",
sim_params.time,
sim_params.delta_time,
sim_params.virtual_time,
sim_params.virtual_delta_time,
sim_params.real_time,
sim_params.real_delta_time,
);
}
struct GpuLimits {
storage_buffer_align: NonZeroU32,
effect_metadata_aligned_size: NonZeroU32,
}
impl GpuLimits {
pub fn from_device(render_device: &RenderDevice) -> Self {
let storage_buffer_align =
render_device.limits().min_storage_buffer_offset_alignment as u64;
let effect_metadata_aligned_size = NonZeroU32::new(
GpuEffectMetadata::min_size()
.get()
.next_multiple_of(storage_buffer_align) as u32,
)
.unwrap();
trace!(
"GPU-aligned sizes (align: {} B):\n- GpuEffectMetadata: {} B -> {} B",
storage_buffer_align,
GpuEffectMetadata::min_size().get(),
effect_metadata_aligned_size.get(),
);
Self {
storage_buffer_align: NonZeroU32::new(storage_buffer_align as u32).unwrap(),
effect_metadata_aligned_size,
}
}
pub fn storage_buffer_align(&self) -> NonZeroU32 {
self.storage_buffer_align
}
pub fn effect_metadata_offset(&self, buffer_index: u32) -> u64 {
self.effect_metadata_aligned_size.get() as u64 * buffer_index as u64
}
}
#[derive(Resource)]
pub struct EffectsMeta {
view_bind_group: Option<BindGroup>,
update_sim_params_bind_group: Option<BindGroup>,
init_and_indirect_sim_params_bind_group: Option<BindGroup>,
indirect_metadata_bind_group: Option<BindGroup>,
indirect_spawner_bind_group: Option<BindGroup>,
sim_params_uniforms: UniformBuffer<GpuSimParams>,
spawner_buffer: AlignedBufferVec<GpuSpawnerParams>,
dispatch_indirect_buffer: GpuBuffer<GpuDispatchIndirectArgs>,
draw_indirect_buffer: BufferTable<GpuDrawIndexedIndirectArgs>,
batch_info_buffer: AlignedBufferVec<GpuBatchInfo>,
is_batch_open: bool,
prefix_sum_buffer: BufferVec<u32>,
effect_metadata_buffer: BufferTable<GpuEffectMetadata>,
gpu_limits: GpuLimits,
indirect_shader_noevent: Handle<Shader>,
indirect_shader_events: Handle<Shader>,
indirect_pipeline_ids: [CachedComputePipelineId; 2],
active_indirect_pipeline_id: CachedComputePipelineId,
prefix_sum_shader: Handle<Shader>,
prefix_sum_bind_group: Option<BindGroup>,
prefix_sum_pipeline_id: CachedComputePipelineId,
}
impl EffectsMeta {
pub fn new(
device: RenderDevice,
indirect_shader_noevent: Handle<Shader>,
indirect_shader_events: Handle<Shader>,
prefix_sum_shader: Handle<Shader>,
) -> Self {
let gpu_limits = GpuLimits::from_device(&device);
let item_align = gpu_limits.storage_buffer_align();
trace!(
"Aligning storage buffers to {} bytes as device limits requires.",
item_align.get()
);
let mut prefix_sum_buffer = BufferVec::new(BufferUsages::STORAGE);
prefix_sum_buffer.set_label(Some("prefix_sum_buffer"));
Self {
view_bind_group: None,
update_sim_params_bind_group: None,
init_and_indirect_sim_params_bind_group: None,
indirect_metadata_bind_group: None,
indirect_spawner_bind_group: None,
sim_params_uniforms: UniformBuffer::default(),
spawner_buffer: AlignedBufferVec::new(
BufferUsages::STORAGE,
Some(item_align.into()),
Some("hanabi:buffer:spawner".to_string()),
),
dispatch_indirect_buffer: GpuBuffer::new(
BufferUsages::STORAGE | BufferUsages::INDIRECT,
Some("hanabi:buffer:dispatch_indirect".to_string()),
),
draw_indirect_buffer: BufferTable::new(
BufferUsages::STORAGE | BufferUsages::INDIRECT,
Some(GpuDrawIndexedIndirectArgs::SHADER_SIZE),
Some("hanabi:buffer:draw_indirect".to_string()),
),
batch_info_buffer: AlignedBufferVec::new(
BufferUsages::STORAGE,
Some(item_align.into()),
Some("hanabi:buffer:batch_info".to_string()),
),
is_batch_open: false,
prefix_sum_buffer,
effect_metadata_buffer: BufferTable::new(
BufferUsages::STORAGE | BufferUsages::INDIRECT,
Some(item_align.into()),
Some("hanabi:buffer:effect_metadata".to_string()),
),
gpu_limits,
indirect_shader_noevent,
indirect_shader_events,
indirect_pipeline_ids: [
CachedComputePipelineId::INVALID,
CachedComputePipelineId::INVALID,
],
active_indirect_pipeline_id: CachedComputePipelineId::INVALID,
prefix_sum_shader,
prefix_sum_bind_group: None,
prefix_sum_pipeline_id: CachedComputePipelineId::INVALID,
}
}
pub fn begin_batch(&mut self, base_particle: u32, spawner_base: u32) -> u32 {
assert!(!self.is_batch_open, "Duplicate call to begin_batch()");
let prefix_sum_offset = self.prefix_sum_buffer.len() as u32;
let batch_info_base = self.batch_info_buffer.len() as u32;
let batch_info = GpuBatchInfo {
total_spawn_count: 0,
total_update_count: 0,
base_effect: spawner_base,
base_particle,
prefix_sum_offset,
prefix_sum_count: u32::MAX, };
trace!("batch info = {:?}", batch_info);
self.batch_info_buffer.push(batch_info);
self.is_batch_open = true;
let di_index = self.dispatch_indirect_buffer.allocate();
assert_eq!(di_index, batch_info_base);
batch_info_base
}
pub fn add_effect_to_batch(&mut self, slab_offset: u32) {
assert!(
self.is_batch_open,
"Cannot add effect before calling begin_batch()"
);
self.prefix_sum_buffer.push(slab_offset);
}
pub fn end_batch(&mut self) {
assert!(
self.is_batch_open,
"Call to end_batch() without begin_batch()"
);
let batch = self
.batch_info_buffer
.last_mut()
.expect("No open batch. Missing begin_batch() call?");
let end = self.prefix_sum_buffer.len() as u32;
assert!(end >= batch.prefix_sum_offset);
batch.prefix_sum_count = end - batch.prefix_sum_offset;
self.is_batch_open = false;
}
pub fn allocate_spawner(&mut self, gpu_spawner_params: GpuSpawnerParams) -> u32 {
let spawner_base = self.spawner_buffer.len() as u32;
self.spawner_buffer.push(gpu_spawner_params);
spawner_base
}
pub fn allocate_draw_indirect(
&mut self,
draw_args: &AnyDrawIndirectArgs,
) -> CachedDrawIndirectArgs {
let row = self
.draw_indirect_buffer
.insert(draw_args.bitcast_to_row_entry());
CachedDrawIndirectArgs {
row,
args: *draw_args,
}
}
pub fn update_draw_indirect(&mut self, row_index: &CachedDrawIndirectArgs) {
self.draw_indirect_buffer
.update(row_index.get_row(), row_index.args.bitcast_to_row_entry());
}
pub fn free_draw_indirect(&mut self, row_index: &CachedDrawIndirectArgs) {
self.draw_indirect_buffer.remove(row_index.get_row());
}
}
bitflags! {
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct LayoutFlags: u32 {
const NONE = 0;
const LOCAL_SPACE_SIMULATION = (1 << 2);
const USE_ALPHA_MASK = (1 << 3);
const FLIPBOOK = (1 << 4);
const NEEDS_UV = (1 << 5);
const RIBBONS = (1 << 6);
const NEEDS_NORMAL = (1 << 7);
const OPAQUE = (1 << 8);
const EMIT_GPU_SPAWN_EVENTS = (1 << 9);
const CONSUME_GPU_SPAWN_EVENTS = (1 << 10);
const READ_PARENT_PARTICLE = (1 << 11);
const NEEDS_PARTICLE_FRAGMENT = (1 << 12);
}
}
impl Default for LayoutFlags {
fn default() -> Self {
Self::NONE
}
}
pub(crate) fn on_remove_cached_effect(
trigger: On<Remove, CachedEffect>,
query: Query<(
Entity,
&MainEntity,
&CachedEffect,
Option<&CachedEffectProperties>,
Option<&CachedParentInfo>,
Option<&CachedEffectEvents>,
)>,
mut effect_cache: ResMut<EffectCache>,
mut effect_bind_groups: ResMut<EffectBindGroups>,
mut event_cache: ResMut<EventCache>,
) {
#[cfg(feature = "trace")]
let _span = bevy::log::info_span!("on_remove_cached_effect").entered();
let Ok((
render_entity,
main_entity,
cached_effect,
_opt_props,
_opt_parent,
opt_cached_effect_events,
)) = query.get(trigger.event().entity)
else {
return;
};
if let Some(cached_effect_events) = opt_cached_effect_events {
match event_cache.free(cached_effect_events) {
Err(err) => {
error!("Error while freeing effect event slice: {err:?}");
}
Ok(buffer_state) => {
if buffer_state != SlabState::Used {
effect_bind_groups.init_metadata_bind_groups.clear();
effect_bind_groups.update_metadata_bind_groups.clear();
}
}
}
}
trace!(
"=> ParticleEffect on render entity {:?} associated with main entity {:?}, removing...",
render_entity,
main_entity,
);
let Ok(SlabState::Free) = effect_cache.remove(cached_effect) else {
return;
};
trace!(
"=> GPU particle slab #{} gone, destroying its bind groups...",
cached_effect.slab_id.index()
);
effect_bind_groups
.particle_slabs
.remove(&cached_effect.slab_id);
}
pub(crate) fn on_remove_cached_metadata(
trigger: On<Remove, CachedEffectMetadata>,
query: Query<&CachedEffectMetadata>,
mut effects_meta: ResMut<EffectsMeta>,
) {
#[cfg(feature = "trace")]
let _span = bevy::log::info_span!("on_remove_cached_metadata").entered();
if let Ok(cached_metadata) = query.get(trigger.event().entity) {
if cached_metadata.table_id.is_valid() {
effects_meta
.effect_metadata_buffer
.remove(cached_metadata.table_id);
}
};
}
pub(crate) fn on_remove_cached_draw_indirect_args(
trigger: On<Remove, CachedDrawIndirectArgs>,
query: Query<&CachedDrawIndirectArgs>,
mut effects_meta: ResMut<EffectsMeta>,
) {
#[cfg(feature = "trace")]
let _span = bevy::log::info_span!("on_remove_cached_draw_indirect_args").entered();
if let Ok(cached_draw_args) = query.get(trigger.event().entity) {
effects_meta.free_draw_indirect(cached_draw_args);
};
}
pub(crate) fn clear_previous_frame_resizes(
mut effects_meta: ResMut<EffectsMeta>,
mut sort_bind_groups: ResMut<SortBindGroups>,
mut init_fill_dispatch_queue: ResMut<InitFillDispatchQueue>,
mut sort_fill_dispatch_queue: ResMut<SortFillDispatchQueue>,
) {
#[cfg(feature = "trace")]
let _span = bevy::log::info_span!("clear_previous_frame_resizes").entered();
trace!("clear_previous_frame_resizes");
init_fill_dispatch_queue.clear();
sort_fill_dispatch_queue.clear();
effects_meta
.dispatch_indirect_buffer
.clear_previous_frame_resizes();
effects_meta
.draw_indirect_buffer
.clear_previous_frame_resizes();
effects_meta
.effect_metadata_buffer
.clear_previous_frame_resizes();
sort_bind_groups.clear_previous_frame_resizes();
}
pub fn fixup_parents(
q_changed_parents: Query<(Entity, Ref<CachedParentInfo>)>,
mut q_children: Query<&mut CachedChildInfo>,
) {
#[cfg(feature = "trace")]
let _span = bevy::log::info_span!("fixup_parents").entered();
trace!("fixup_parents");
trace!(
"Updating the global index of children of parent effects whose child list just changed..."
);
for (parent_entity, cached_parent_info) in q_changed_parents.iter() {
let base_index =
cached_parent_info.byte_range.start / GpuChildInfo::SHADER_SIZE.get() as u32;
let parent_changed = cached_parent_info.is_changed();
trace!(
"Updating {} children of parent effect {:?} with base child index {} (parent_changed:{})...",
cached_parent_info.children.len(),
parent_entity,
base_index,
parent_changed
);
for (child_entity, _) in &cached_parent_info.children {
let Ok(mut cached_child_info) = q_children.get_mut(*child_entity) else {
error!(
"Cannot find child {:?} declared by parent {:?}",
*child_entity, parent_entity
);
continue;
};
if !cached_child_info.is_changed() && !parent_changed {
continue;
}
cached_child_info.global_child_index = base_index + cached_child_info.local_child_index;
trace!(
"+ Updated global index for child ID {:?} of parent {:?}: local={}, global={}",
child_entity,
parent_entity,
cached_child_info.local_child_index,
cached_child_info.global_child_index
);
}
}
}
pub fn allocate_effects(
mut commands: Commands,
mut q_extracted_effects: Query<
(
Entity,
&ExtractedEffect,
Has<ChildEffectOf>,
Option<&mut CachedEffect>,
),
Changed<ExtractedEffect>,
>,
mut effect_cache: ResMut<EffectCache>,
) {
#[cfg(feature = "trace")]
let _span = bevy::log::info_span!("allocate_effects").entered();
trace!("allocate_effects");
for (entity, extracted_effect, has_parent, maybe_cached_effect) in &mut q_extracted_effects {
if let Some(mut cached_effect) = maybe_cached_effect {
trace!("Updating EffectCache entry for entity {entity:?}...");
let _ = effect_cache.remove(cached_effect.as_ref());
*cached_effect = effect_cache.insert(
extracted_effect.handle.clone(),
extracted_effect.capacity,
&extracted_effect.particle_layout,
);
} else {
trace!("Allocating new entry in EffectCache for entity {entity:?}...");
let cached_effect = effect_cache.insert(
extracted_effect.handle.clone(),
extracted_effect.capacity,
&extracted_effect.particle_layout,
);
commands.entity(entity).insert(cached_effect);
}
if !has_parent {
let parent_min_binding_size = None;
effect_cache.ensure_particle_bind_group_layout_desc(
extracted_effect.particle_layout.min_binding_size32(),
parent_min_binding_size,
);
}
{
let consume_gpu_spawn_events = extracted_effect
.layout_flags
.contains(LayoutFlags::CONSUME_GPU_SPAWN_EVENTS);
effect_cache.ensure_metadata_init_bind_group_layout_desc(consume_gpu_spawn_events);
}
}
}
pub fn update_mesh_locations(
mut commands: Commands,
mut effects_meta: ResMut<EffectsMeta>,
mesh_allocator: Res<MeshAllocator>,
render_meshes: Res<RenderAssets<RenderMesh>>,
mut q_cached_effects: Query<(
Entity,
&ExtractedEffectMesh,
Option<&mut CachedMeshLocation>,
Option<&mut CachedDrawIndirectArgs>,
)>,
) {
#[cfg(feature = "trace")]
let _span = bevy::log::info_span!("update_mesh_locations").entered();
trace!("update_mesh_locations");
for (entity, extracted_mesh, maybe_cached_mesh_location, maybe_cached_draw_indirect_args) in
&mut q_cached_effects
{
let mut cmds = commands.entity(entity);
let Some(render_mesh) = render_meshes.get(extracted_mesh.mesh) else {
warn!(
"Cannot find render mesh of particle effect instance on entity {:?}, despite applying default mesh. Invalid asset handle: {:?}",
entity, extracted_mesh.mesh
);
cmds.remove::<CachedMeshLocation>();
continue;
};
let Some(mesh_vertex_buffer_slice) = mesh_allocator.mesh_vertex_slice(&extracted_mesh.mesh)
else {
trace!(
"Effect main_entity {:?}: cannot find vertex slice of render mesh {:?}",
entity,
extracted_mesh.mesh
);
cmds.remove::<CachedMeshLocation>();
continue;
};
let mesh_index_buffer_slice = mesh_allocator.mesh_index_slice(&extracted_mesh.mesh);
let indexed =
if let RenderMeshBufferInfo::Indexed { index_format, .. } = render_mesh.buffer_info {
if let Some(ref slice) = mesh_index_buffer_slice {
Some(MeshIndexSlice {
format: index_format,
buffer: slice.buffer.clone(),
range: slice.range.clone(),
})
} else {
trace!(
"Effect main_entity {:?}: cannot find index slice of render mesh {:?}",
entity,
extracted_mesh.mesh
);
cmds.remove::<CachedMeshLocation>();
continue;
}
} else {
None
};
let new_draw_args = AnyDrawIndirectArgs::from_slices(
&mesh_vertex_buffer_slice,
mesh_index_buffer_slice.as_ref(),
);
let new_mesh_location = match &mesh_index_buffer_slice {
Some(mesh_index_buffer_slice) => CachedMeshLocation {
vertex_buffer: mesh_vertex_buffer_slice.buffer.id(),
vertex_or_index_count: mesh_index_buffer_slice.range.len() as u32,
first_index_or_vertex_offset: mesh_index_buffer_slice.range.start,
vertex_offset_or_base_instance: mesh_vertex_buffer_slice.range.start as i32,
indexed,
},
None => CachedMeshLocation {
vertex_buffer: mesh_vertex_buffer_slice.buffer.id(),
vertex_or_index_count: mesh_vertex_buffer_slice.range.len() as u32,
first_index_or_vertex_offset: mesh_vertex_buffer_slice.range.start,
vertex_offset_or_base_instance: 0,
indexed: None,
},
};
if let Some(mut cached_draw_indirect) = maybe_cached_draw_indirect_args {
assert!(cached_draw_indirect.row.is_valid());
if new_draw_args != cached_draw_indirect.args {
debug!(
"Indirect draw args changed for asset {:?}\nold:{:?}\nnew:{:?}",
entity, cached_draw_indirect.args, new_draw_args
);
cached_draw_indirect.args = new_draw_args;
effects_meta.update_draw_indirect(cached_draw_indirect.as_ref());
}
} else {
cmds.insert(effects_meta.allocate_draw_indirect(&new_draw_args));
}
if let Some(mut old_mesh_location) = maybe_cached_mesh_location {
if *old_mesh_location != new_mesh_location {
debug!(
"Mesh location changed for asset {:?}\nold:{:?}\nnew:{:?}",
entity, old_mesh_location, new_mesh_location
);
*old_mesh_location = new_mesh_location;
}
} else {
cmds.insert(new_mesh_location);
}
}
}
pub fn allocate_metadata(
mut effects_meta: ResMut<EffectsMeta>,
mut q_metadata: Query<&mut CachedEffectMetadata>,
) {
for mut metadata in &mut q_metadata {
if !metadata.table_id.is_valid() {
metadata.table_id = effects_meta
.effect_metadata_buffer
.insert(metadata.metadata);
} else {
}
}
}
pub fn allocate_parent_child_infos(
mut commands: Commands,
mut effect_cache: ResMut<EffectCache>,
mut event_cache: ResMut<EventCache>,
mut q_child_effects: Query<(
Entity,
&ExtractedEffect,
&ChildEffectOf,
&CachedEffectEvents,
Option<&mut CachedChildInfo>,
)>,
mut q_parent_effects: Query<(
Entity,
&ExtractedEffect,
&CachedEffect,
&ChildrenEffects,
Option<&mut CachedParentInfo>,
)>,
) {
#[cfg(feature = "trace")]
let _span = bevy::log::info_span!("allocate_child_infos").entered();
trace!("allocate_child_infos");
for (child_entity, _, child_effect_of, cached_effect_events, maybe_cached_child_info) in
&mut q_child_effects
{
let parent_entity = child_effect_of.parent;
let Ok((_, _, parent_cached_effect, children_effects, _)) =
q_parent_effects.get(parent_entity)
else {
warn!("Unknown parent #{parent_entity:?} on child entity {child_entity:?}, removing CachedChildInfo.");
if maybe_cached_child_info.is_some() {
commands.entity(child_entity).remove::<CachedChildInfo>();
}
continue;
};
let Some(local_child_index) = children_effects.0.iter().position(|e| *e == child_entity)
else {
warn!("Cannot find child entity {child_entity:?} in the children collection of parent entity {parent_entity:?}. Relationship desync?");
if maybe_cached_child_info.is_some() {
commands.entity(child_entity).remove::<CachedChildInfo>();
}
continue;
};
let local_child_index = local_child_index as u32;
let Some(parent_buffer_binding_source) = effect_cache
.get_slab(&parent_cached_effect.slab_id)
.map(|effect_buffer| effect_buffer.max_binding_source())
else {
warn!(
"Unknown parent slab #{} on parent entity {:?}, removing CachedChildInfo.",
parent_cached_effect.slab_id.index(),
parent_entity
);
if maybe_cached_child_info.is_some() {
commands.entity(child_entity).remove::<CachedChildInfo>();
}
continue;
};
let new_cached_child_info = CachedChildInfo {
parent_slab_id: parent_cached_effect.slab_id,
parent_slab_offset: parent_cached_effect.slice.range().start,
parent_particle_layout: parent_cached_effect.slice.particle_layout.clone(),
parent_buffer_binding_source,
local_child_index,
global_child_index: u32::MAX, init_indirect_dispatch_index: cached_effect_events.init_indirect_dispatch_index,
};
if let Some(mut cached_child_info) = maybe_cached_child_info {
if !cached_child_info.is_locally_equal(&new_cached_child_info) {
*cached_child_info = new_cached_child_info;
}
} else {
commands.entity(child_entity).insert(new_cached_child_info);
}
}
for (parent_entity, parent_extracted_effect, _, children_effects, maybe_cached_parent_info) in
&mut q_parent_effects
{
let parent_min_binding_size = parent_extracted_effect.particle_layout.min_binding_size32();
let mut new_children = Vec::with_capacity(children_effects.0.len());
let mut new_child_infos = Vec::with_capacity(children_effects.0.len());
for child_entity in children_effects.0.iter() {
let Ok((_, child_extracted_effect, _, cached_effect_events, _)) =
q_child_effects.get(*child_entity)
else {
warn!("Child entity {child_entity:?} from parent entity {parent_entity:?} didnt't resolve to a child instance. The parent effect cannot be processed.");
if maybe_cached_parent_info.is_some() {
commands.entity(parent_entity).remove::<CachedParentInfo>();
}
break;
};
let Some(event_buffer) = event_cache.get_buffer(cached_effect_events.buffer_index)
else {
warn!("Child entity {child_entity:?} from parent entity {parent_entity:?} doesn't have an allocated GPU event buffer. The parent effect cannot be processed.");
break;
};
let buffer_binding_source = BufferBindingSource {
buffer: event_buffer.clone(),
offset: cached_effect_events.range.start,
size: NonZeroU32::new(cached_effect_events.range.len() as u32).unwrap(),
};
new_children.push((*child_entity, buffer_binding_source));
new_child_infos.push(GpuChildInfo {
event_count: 0,
init_indirect_dispatch_index: cached_effect_events.init_indirect_dispatch_index,
});
effect_cache.ensure_particle_bind_group_layout_desc(
child_extracted_effect.particle_layout.min_binding_size32(),
Some(parent_min_binding_size),
);
}
debug_assert_eq!(new_children.len(), new_child_infos.len());
if (new_children.len() < children_effects.len()) && maybe_cached_parent_info.is_some() {
warn!("One or more child effect(s) on parent effect {parent_entity:?} failed to configure. The parent effect cannot be processed.");
commands.entity(parent_entity).remove::<CachedParentInfo>();
continue;
}
if let Some(mut cached_parent_info) = maybe_cached_parent_info {
if cached_parent_info.children != new_children {
event_cache.reallocate_child_infos(
parent_entity,
new_children,
&new_child_infos[..],
cached_parent_info.as_mut(),
);
}
} else {
let cached_parent_info =
event_cache.allocate_child_infos(parent_entity, new_children, &new_child_infos[..]);
commands.entity(parent_entity).insert(cached_parent_info);
}
}
}
pub fn prepare_init_update_pipelines(
mut q_effects: Query<(
Entity,
&ExtractedEffect,
&CachedEffect,
Option<&CachedChildInfo>,
Option<&CachedParentInfo>,
Option<&CachedEffectProperties>,
&mut CachedPipelines,
)>,
mut effect_cache: ResMut<EffectCache>,
pipeline_cache: Res<PipelineCache>,
property_cache: ResMut<PropertyCache>,
init_pipeline: Res<ParticlesInitPipeline>,
update_pipeline: Res<ParticlesUpdatePipeline>,
mut specialized_init_pipelines: ResMut<SpecializedComputePipelines<ParticlesInitPipeline>>,
mut specialized_update_pipelines: ResMut<SpecializedComputePipelines<ParticlesUpdatePipeline>>,
) {
#[cfg(feature = "trace")]
let _span = bevy::log::info_span!("prepare_init_update_pipelines").entered();
trace!("prepare_init_update_pipelines");
for (
entity,
extracted_effect,
cached_effect,
maybe_cached_child_info,
maybe_cached_parent_info,
maybe_cached_properties,
mut cached_pipelines,
) in &mut q_effects
{
trace!(
"Preparing pipelines for effect {:?}... (flags: {:?})",
entity,
cached_pipelines.flags
);
let particle_layout = &cached_effect.slice.particle_layout;
let particle_layout_min_binding_size = particle_layout.min_binding_size32();
let has_event_buffer = maybe_cached_child_info.is_some();
let parent_particle_layout_min_binding_size = maybe_cached_child_info
.as_ref()
.map(|cci| cci.parent_particle_layout.min_binding_size32());
let Some(particle_bind_group_layout_desc) = effect_cache.particle_bind_group_layout_desc(
particle_layout_min_binding_size,
parent_particle_layout_min_binding_size,
) else {
error!("Failed to find particle sim bind group @1 for min_binding_size={} parent_min_binding_size={:?}",
particle_layout_min_binding_size, parent_particle_layout_min_binding_size);
continue;
};
let particle_bind_group_layout_desc = particle_bind_group_layout_desc.clone();
let property_layout_min_binding_size =
maybe_cached_properties.map(|cp| cp.property_layout.min_binding_size());
let spawner_bind_group_layout_desc = property_cache
.bind_group_layout_desc(property_layout_min_binding_size)
.unwrap_or_else(|| {
panic!(
"Failed to find spawner@2 bind group layout for property binding size {:?}",
property_layout_min_binding_size,
)
});
trace!(
"Retrieved spawner@2 bind group layout desc for property binding size {}: {:?}.",
property_layout_min_binding_size
.as_ref()
.map(|size| size.get())
.unwrap_or(0),
spawner_bind_group_layout_desc,
);
let init_pipeline_id = if let Some(init_pipeline_id) = cached_pipelines.init.as_ref() {
*init_pipeline_id
} else {
cached_pipelines
.flags
.remove(CachedPipelineFlags::INIT_PIPELINE_READY);
let metadata_bind_group_layout_desc = effect_cache
.metadata_init_bind_group_layout_desc(has_event_buffer)
.unwrap()
.clone();
let init_pipeline_key_flags = {
let mut flags = ParticleInitPipelineKeyFlags::empty();
flags.set(
ParticleInitPipelineKeyFlags::ATTRIBUTE_PREV,
particle_layout.contains(Attribute::PREV),
);
flags.set(
ParticleInitPipelineKeyFlags::ATTRIBUTE_NEXT,
particle_layout.contains(Attribute::NEXT),
);
flags.set(
ParticleInitPipelineKeyFlags::CONSUME_GPU_SPAWN_EVENTS,
has_event_buffer,
);
flags
};
let init_pipeline_id: CachedComputePipelineId = specialized_init_pipelines.specialize(
pipeline_cache.as_ref(),
&init_pipeline,
ParticleInitPipelineKey {
shader: extracted_effect.effect_shaders.init.clone(),
particle_layout_min_binding_size,
parent_particle_layout_min_binding_size,
flags: init_pipeline_key_flags,
particle_bind_group_layout_desc: particle_bind_group_layout_desc.clone(),
spawner_bind_group_layout_desc: spawner_bind_group_layout_desc.clone(),
metadata_bind_group_layout_desc,
},
);
trace!("Init pipeline specialized: id={:?}", init_pipeline_id);
cached_pipelines.init = Some(init_pipeline_id);
init_pipeline_id
};
let update_pipeline_id = if let Some(update_pipeline_id) = cached_pipelines.update.as_ref()
{
*update_pipeline_id
} else {
cached_pipelines
.flags
.remove(CachedPipelineFlags::UPDATE_PIPELINE_READY);
let num_event_buffers = maybe_cached_parent_info
.as_ref()
.map(|p| p.children.len() as u32)
.unwrap_or_default();
effect_cache.ensure_metadata_update_bind_group_layout_desc(num_event_buffers);
let metadata_bind_group_layout_desc = effect_cache
.metadata_update_bind_group_layout_desc(num_event_buffers)
.unwrap()
.clone();
let update_pipeline_id = specialized_update_pipelines.specialize(
pipeline_cache.as_ref(),
&update_pipeline,
ParticleUpdatePipelineKey {
shader: extracted_effect.effect_shaders.update.clone(),
particle_layout: particle_layout.clone(),
parent_particle_layout_min_binding_size,
num_event_buffers,
particle_bind_group_layout_desc: particle_bind_group_layout_desc.clone(),
spawner_bind_group_layout_desc: spawner_bind_group_layout_desc.clone(),
metadata_bind_group_layout_desc,
},
);
trace!("Update pipeline specialized: id={:?}", update_pipeline_id);
cached_pipelines.update = Some(update_pipeline_id);
update_pipeline_id
};
if pipeline_cache
.get_compute_pipeline(init_pipeline_id)
.is_none()
{
trace!(
"Skipping effect from render entity {:?} due to missing or not ready init pipeline (status: {:?})",
entity,
pipeline_cache.get_compute_pipeline_state(init_pipeline_id)
);
cached_pipelines
.flags
.remove(CachedPipelineFlags::INIT_PIPELINE_READY);
continue;
}
cached_pipelines
.flags
.insert(CachedPipelineFlags::INIT_PIPELINE_READY);
trace!("[Effect {:?}] Init pipeline ready.", entity);
if pipeline_cache
.get_compute_pipeline(update_pipeline_id)
.is_none()
{
trace!(
"Skipping effect from render entity {:?} due to missing or not ready update pipeline (status: {:?})",
entity,
pipeline_cache.get_compute_pipeline_state(update_pipeline_id)
);
cached_pipelines
.flags
.remove(CachedPipelineFlags::UPDATE_PIPELINE_READY);
continue;
}
cached_pipelines
.flags
.insert(CachedPipelineFlags::UPDATE_PIPELINE_READY);
trace!("[Effect {:?}] Update pipeline ready.", entity);
}
}
pub fn prepare_indirect_pipeline(
event_cache: Res<EventCache>,
mut effects_meta: ResMut<EffectsMeta>,
pipeline_cache: Res<PipelineCache>,
indirect_pipeline: Res<DispatchIndirectPipeline>,
mut specialized_indirect_pipelines: ResMut<
SpecializedComputePipelines<DispatchIndirectPipeline>,
>,
prefix_sum_pipeline: Res<PrefixSumPipeline>,
) {
if effects_meta.prefix_sum_pipeline_id == CachedComputePipelineId::INVALID {
effects_meta.prefix_sum_pipeline_id =
pipeline_cache.queue_compute_pipeline(prefix_sum_pipeline.make_pipeline_desc())
}
if effects_meta.indirect_pipeline_ids[0] == CachedComputePipelineId::INVALID {
effects_meta.indirect_pipeline_ids[0] = specialized_indirect_pipelines.specialize(
pipeline_cache.as_ref(),
&indirect_pipeline,
DispatchIndirectPipelineKey { has_events: false },
);
}
if effects_meta.indirect_pipeline_ids[1] == CachedComputePipelineId::INVALID {
effects_meta.indirect_pipeline_ids[1] = specialized_indirect_pipelines.specialize(
pipeline_cache.as_ref(),
&indirect_pipeline,
DispatchIndirectPipelineKey { has_events: true },
);
}
let is_empty = event_cache.child_infos().is_empty();
if effects_meta.active_indirect_pipeline_id == CachedComputePipelineId::INVALID {
if is_empty {
effects_meta.active_indirect_pipeline_id = effects_meta.indirect_pipeline_ids[0];
} else {
effects_meta.active_indirect_pipeline_id = effects_meta.indirect_pipeline_ids[1];
}
} else {
let was_empty =
effects_meta.active_indirect_pipeline_id == effects_meta.indirect_pipeline_ids[0];
if was_empty && !is_empty {
trace!("First event buffer inserted; switching indirect pass to event mode...");
effects_meta.active_indirect_pipeline_id = effects_meta.indirect_pipeline_ids[1];
} else if is_empty && !was_empty {
trace!("Last event buffer removed; switching indirect pass to no-event mode...");
effects_meta.active_indirect_pipeline_id = effects_meta.indirect_pipeline_ids[0];
}
}
}
pub fn clear_transient_batch_inputs(
mut commands: Commands,
mut q_cached_effects: Query<Entity, With<BatchInput>>,
) {
for entity in &mut q_cached_effects {
if let Ok(mut cmd) = commands.get_entity(entity) {
cmd.remove::<BatchInput>();
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Component)]
pub(crate) struct ExtractedEffectMesh {
pub mesh: AssetId<Mesh>,
}
#[derive(Debug, Clone)]
#[allow(dead_code)]
pub(crate) struct MeshIndexSlice {
pub format: IndexFormat,
pub buffer: Buffer,
pub range: Range<u32>,
}
impl PartialEq for MeshIndexSlice {
fn eq(&self, other: &Self) -> bool {
self.format == other.format
&& self.buffer.id() == other.buffer.id()
&& self.range == other.range
}
}
impl Eq for MeshIndexSlice {}
#[derive(Debug, Clone, PartialEq, Eq, Component)]
pub(crate) struct CachedMeshLocation {
pub vertex_buffer: BufferId,
pub vertex_or_index_count: u32,
pub first_index_or_vertex_offset: u32,
pub vertex_offset_or_base_instance: i32,
pub indexed: Option<MeshIndexSlice>,
}
bitflags! {
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct CachedPipelineFlags: u8 {
const NONE = 0;
const INIT_PIPELINE_READY = (1u8 << 0);
const UPDATE_PIPELINE_READY = (1u8 << 1);
}
}
impl Default for CachedPipelineFlags {
fn default() -> Self {
Self::NONE
}
}
#[derive(Debug, Default, Component)]
pub(crate) struct CachedPipelines {
pub flags: CachedPipelineFlags,
pub init: Option<CachedComputePipelineId>,
pub update: Option<CachedComputePipelineId>,
}
impl CachedPipelines {
#[inline]
pub fn is_ready(&self) -> bool {
self.flags.contains(
CachedPipelineFlags::INIT_PIPELINE_READY | CachedPipelineFlags::UPDATE_PIPELINE_READY,
)
}
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Component)]
pub(crate) struct CachedReadyState {
is_ready: bool,
}
impl CachedReadyState {
#[inline(always)]
pub fn new(is_ready: bool) -> Self {
Self { is_ready }
}
#[inline(always)]
pub fn and(mut self, ancestors_ready: bool) -> Self {
self.and_with(ancestors_ready);
self
}
#[inline(always)]
pub fn and_with(&mut self, ancestors_ready: bool) {
self.is_ready = self.is_ready && ancestors_ready;
}
#[inline(always)]
pub fn is_ready(&self) -> bool {
self.is_ready
}
}
#[derive(SystemParam)]
pub struct PrepareEffectsReadOnlyParams<'w, 's> {
sim_params: Res<'w, SimParams>,
marker: PhantomData<&'s usize>,
}
pub(crate) fn propagate_ready_state(
mut q_root_effects: Query<
(
Entity,
Option<&ChildrenEffects>,
Ref<CachedPipelines>,
&mut CachedReadyState,
),
Without<ChildEffectOf>,
>,
mut orphaned: RemovedComponents<ChildEffectOf>,
q_ready_state: Query<
(
Ref<CachedPipelines>,
&mut CachedReadyState,
Option<&ChildrenEffects>,
),
With<ChildEffectOf>,
>,
q_child_effects: Query<(Entity, Ref<ChildEffectOf>), With<CachedReadyState>>,
mut orphaned_entities: Local<Vec<Entity>>,
) {
#[cfg(feature = "trace")]
let _span = bevy::log::info_span!("propagate_ready_state").entered();
trace!("propagate_ready_state");
orphaned_entities.clear();
orphaned_entities.extend(orphaned.read());
orphaned_entities.sort_unstable();
q_root_effects.par_iter_mut().for_each(
|(entity, maybe_children, cached_pipelines, mut cached_ready_state)| {
let changed = cached_pipelines.is_changed() || cached_ready_state.is_added() || orphaned_entities.binary_search(&entity).is_ok();
trace!("[Entity {}] changed={} cached_pipelines={} ready_state={}", entity, changed, cached_pipelines.is_ready(), cached_ready_state.is_ready);
if changed {
let new_ready_state = CachedReadyState::new(cached_pipelines.is_ready());
if *cached_ready_state != new_ready_state {
debug!(
"[Entity {}] Changed ready to: {}",
entity,
new_ready_state.is_ready()
);
*cached_ready_state = new_ready_state;
}
}
if let Some(children) = maybe_children {
for (child, child_of) in q_child_effects.iter_many(children) {
assert_eq!(
child_of.parent, entity,
"Malformed hierarchy. This probably means that your hierarchy has been improperly maintained, or contains a cycle"
);
#[expect(unsafe_code, reason = "`propagate_ready_state_recursive()` is unsafe due to its use of `Query::get_unchecked()`.")]
unsafe {
propagate_ready_state_recursive(
&cached_ready_state,
&q_ready_state,
&q_child_effects,
child,
changed || child_of.is_changed(),
);
}
}
}
},
);
}
#[expect(
unsafe_code,
reason = "This function uses `Query::get_unchecked()`, which can result in multiple mutable references if the preconditions are not met."
)]
unsafe fn propagate_ready_state_recursive(
parent_state: &CachedReadyState,
q_ready_state: &Query<
(
Ref<CachedPipelines>,
&mut CachedReadyState,
Option<&ChildrenEffects>,
),
With<ChildEffectOf>,
>,
q_child_of: &Query<(Entity, Ref<ChildEffectOf>), With<CachedReadyState>>,
entity: Entity,
mut changed: bool,
) {
let (cached_ready_state, maybe_children) = {
let Ok((cached_pipelines, mut cached_ready_state, maybe_children)) =
(unsafe { q_ready_state.get_unchecked(entity) }) else {
return;
};
changed |= cached_pipelines.is_changed() || cached_ready_state.is_added();
if changed {
let new_ready_state =
CachedReadyState::new(parent_state.is_ready()).and(cached_pipelines.is_ready());
if *cached_ready_state != new_ready_state {
debug!(
"[Entity {}] Changed ready to: {}",
entity,
new_ready_state.is_ready()
);
*cached_ready_state = new_ready_state;
}
}
(cached_ready_state, maybe_children)
};
let Some(children) = maybe_children else {
return;
};
for (child, child_of) in q_child_of.iter_many(children) {
assert_eq!(
child_of.parent, entity,
"Malformed hierarchy. This probably means that your hierarchy has been improperly maintained, or contains a cycle"
);
unsafe {
propagate_ready_state_recursive(
cached_ready_state.as_ref(),
q_ready_state,
q_child_of,
child,
changed || child_of.is_changed(),
);
}
}
}
pub(crate) fn prepare_batch_inputs(
mut commands: Commands,
read_only_params: PrepareEffectsReadOnlyParams,
pipeline_cache: Res<PipelineCache>,
mut effects_meta: ResMut<EffectsMeta>,
q_cached_effects: Query<(
MainEntity,
Entity,
&ExtractedEffect,
&ExtractedSpawner,
&CachedEffect,
&CachedEffectMetadata,
&CachedReadyState,
&CachedPipelines,
Option<&CachedDrawIndirectArgs>,
Option<&CachedParentInfo>,
Option<&ChildEffectOf>,
Option<&CachedChildInfo>,
Option<&CachedEffectEvents>,
)>,
mut sort_bind_groups: ResMut<SortBindGroups>,
) {
#[cfg(feature = "trace")]
let _span = bevy::log::info_span!("prepare_batch_inputs").entered();
trace!("prepare_batch_inputs");
let sim_params = read_only_params.sim_params.into_inner();
effects_meta.spawner_buffer.clear();
effects_meta.dispatch_indirect_buffer.clear();
let mut extracted_effect_count = 0;
let mut prepared_effect_count = 0;
for (
main_entity,
render_entity,
extracted_effect,
extracted_spawner,
cached_effect,
cached_effect_metadata,
cached_ready_state,
cached_pipelines,
maybe_cached_draw_indirect_args,
maybe_cached_parent_info,
maybe_child_effect_of,
maybe_cached_child_info,
maybe_cached_effect_events,
) in &q_cached_effects
{
extracted_effect_count += 1;
if !cached_ready_state.is_ready() {
trace!("Pipelines not ready for effect {}, skipped.", render_entity);
continue;
}
if !extracted_spawner.is_visible
&& (extracted_effect.simulation_condition == SimulationCondition::WhenVisible)
{
trace!(
"Effect {} not visible, and simulation condition is WhenVisible, so skipped.",
render_entity
);
continue;
}
let init_and_update_pipeline_ids = InitAndUpdatePipelineIds {
init: cached_pipelines.init.unwrap(),
update: cached_pipelines.update.unwrap(),
};
let effect_slice = EffectSlice {
slice: cached_effect.slice.range(),
slab_id: cached_effect.slab_id,
particle_layout: cached_effect.slice.particle_layout.clone(),
};
trace!("child_effect_of={:?}", maybe_child_effect_of);
let parent_slab_id = if let Some(child_effect_of) = maybe_child_effect_of {
let Ok((_, _, _, _, parent_cached_effect, _, _, _, _, _, _, _, _)) =
q_cached_effects.get(child_effect_of.parent)
else {
error!(
"Effect main_entity {:?}: parent render entity {:?} not found.",
main_entity, child_effect_of.parent
);
continue;
};
Some(parent_cached_effect.slab_id)
} else {
None
};
if extracted_effect.layout_flags.contains(LayoutFlags::RIBBONS) {
if let Err(err) = sort_bind_groups.ensure_sort_fill_bind_group_layout_desc(
&pipeline_cache,
&extracted_effect.particle_layout,
) {
error!(
"Failed to create bind group for ribbon effect sorting: {:?}",
err
);
continue;
}
if !sort_bind_groups
.is_pipeline_ready(&extracted_effect.particle_layout, &pipeline_cache)
{
trace!(
"Sort pipeline not ready for effect on main entity {:?}; skipped.",
main_entity
);
continue;
}
}
trace!("init_shader = {:?}", extracted_effect.effect_shaders.init);
trace!(
"update_shader = {:?}",
extracted_effect.effect_shaders.update
);
trace!(
"render_shader = {:?}",
extracted_effect.effect_shaders.render
);
trace!("layout_flags = {:?}", extracted_effect.layout_flags);
trace!("particle_layout = {:?}", effect_slice.particle_layout);
let parent_slab_offset = maybe_cached_child_info.map(|cci| cci.parent_slab_offset);
assert!(cached_effect_metadata.table_id.is_valid());
let gpu_spawner_params = GpuSpawnerParams::new(
&extracted_spawner.transform,
extracted_spawner.spawn_count,
extracted_spawner.prng_seed,
cached_effect.slice.range().start,
parent_slab_offset,
cached_effect_metadata.table_id,
maybe_cached_draw_indirect_args,
);
trace!("Updating cached effect at entity {render_entity:?}...");
let mut cmd = commands.entity(render_entity);
cmd.insert(BatchInput {
effect_slice,
init_and_update_pipeline_ids,
parent_slab_id,
event_buffer_index: maybe_cached_effect_events.map(|cee| cee.buffer_index),
child_effects: maybe_cached_parent_info
.as_ref()
.map(|cp| cp.children.clone())
.unwrap_or_default(),
gpu_spawner_params,
init_indirect_dispatch_index: maybe_cached_child_info
.as_ref()
.map(|cc| cc.init_indirect_dispatch_index),
});
prepared_effect_count += 1;
}
trace!("Prepared {prepared_effect_count}/{extracted_effect_count} extracted effect(s)");
{
let mut gpu_sim_params: GpuSimParams = sim_params.into();
gpu_sim_params.num_effects = prepared_effect_count;
trace!(
"Simulation parameters: time={} delta_time={} virtual_time={} \
virtual_delta_time={} real_time={} real_delta_time={} num_effects={}",
gpu_sim_params.time,
gpu_sim_params.delta_time,
gpu_sim_params.virtual_time,
gpu_sim_params.virtual_delta_time,
gpu_sim_params.real_time,
gpu_sim_params.real_delta_time,
gpu_sim_params.num_effects,
);
effects_meta.sim_params_uniforms.set(gpu_sim_params);
}
}
pub(crate) fn batch_effects(
render_device: Res<RenderDevice>,
render_queue: Res<RenderQueue>,
mut commands: Commands,
mut effects_meta: ResMut<EffectsMeta>,
mut sort_bind_groups: ResMut<SortBindGroups>,
mut q_cached_effects: Query<(
Entity,
&MainEntity,
&ExtractedEffect,
&ExtractedSpawner,
&ExtractedEffectMesh,
&CachedDrawIndirectArgs,
&CachedEffectMetadata,
Option<&CachedEffectEvents>,
Option<&ChildEffectOf>,
Option<&CachedChildInfo>,
Option<&CachedEffectProperties>,
// The presence of BatchInput ensure the effect is ready
&mut BatchInput,
)>,
mut sorted_effect_batches: ResMut<SortedEffectBatches>,
mut gpu_buffer_operations: ResMut<GpuBufferOperations>,
mut effect_bind_groups: ResMut<EffectBindGroups>,
mut property_bind_groups: ResMut<PropertyBindGroups>,
mut sort_fill_dispatch_queue: ResMut<SortFillDispatchQueue>,
) {
#[cfg(feature = "trace")]
let _span = bevy::log::info_span!("batch_effects").entered();
trace!("batch_effects");
let mut effect_sorter = EffectSorter::new();
for (entity, _, _, _, _, _, _, _, child_of, _, _, input) in &q_cached_effects {
effect_sorter.insert(
entity,
input.effect_slice.slab_id,
input.effect_slice.slice.start,
child_of.map(|co| co.parent),
);
}
effect_sorter.sort();
sort_bind_groups.clear_indirect_dispatch_buffer();
effects_meta.prefix_sum_buffer.clear();
effects_meta.batch_info_buffer.clear();
trace!("Batching {} effects...", q_cached_effects.iter().len());
sorted_effect_batches.clear();
for entity in effect_sorter.effects.iter().map(|e| e.entity) {
let Ok((
entity,
main_entity,
extracted_effect,
extracted_spawner,
extracted_effect_mesh,
cached_draw_indirect_args,
cached_effect_metadata,
cached_effect_events,
_,
cached_child_info,
cached_properties,
mut input,
)) = q_cached_effects.get_mut(entity)
else {
continue;
};
let translation = extracted_spawner.transform.translation();
let spawner_index = effects_meta.allocate_spawner(input.gpu_spawner_params);
let mut effect_batch = EffectBatch::from_input(
main_entity.id(),
extracted_effect,
extracted_spawner,
extracted_effect_mesh,
cached_effect_events,
cached_child_info,
spawner_index,
&mut input,
cached_draw_indirect_args.row,
cached_effect_metadata.table_id,
cached_properties.map(|cp| PropertyBindGroupKey {
buffer_index: cp.buffer_index,
binding_size: cp.property_layout.min_binding_size().get() as u32,
}),
);
if extracted_effect.layout_flags.contains(LayoutFlags::RIBBONS) {
let sort_fill_indirect_dispatch_index = sort_bind_groups.allocate_indirect_dispatch();
effect_batch.sort_fill_indirect_dispatch_index =
Some(sort_fill_indirect_dispatch_index);
sort_fill_dispatch_queue.enqueue(
effect_batch.metadata_table_id,
sort_fill_indirect_dispatch_index,
);
}
let batch_info_id =
effects_meta.begin_batch(effect_batch.slice.start, effect_batch.spawner_base);
effect_batch.batch_info_id = batch_info_id;
effects_meta.add_effect_to_batch(effect_batch.slice.start);
effects_meta.end_batch();
let effect_batch_index = sorted_effect_batches.push(effect_batch);
trace!(
"Spawned effect batch #{:?} with batch-info-id {} from cached instance on entity {:?}.",
effect_batch_index,
batch_info_id,
entity,
);
commands
.spawn(EffectDrawBatch {
effect_batch_index,
translation,
main_entity: *main_entity,
})
.insert(TemporaryRenderEntity);
}
gpu_buffer_operations.begin_frame();
debug_assert!(sorted_effect_batches.dispatch_queue_index.is_none());
if effects_meta
.spawner_buffer
.write_buffer(&render_device, &render_queue)
{
effect_bind_groups.particle_slabs.clear();
property_bind_groups.clear(true);
effects_meta.indirect_spawner_bind_group = None;
}
if effects_meta
.batch_info_buffer
.write_buffer(&render_device, &render_queue)
{
effect_bind_groups.particle_slabs.clear();
property_bind_groups.clear(true);
effects_meta.indirect_spawner_bind_group = None;
effects_meta.prefix_sum_bind_group = None;
}
{
let cpu_len = effects_meta.prefix_sum_buffer.len();
if cpu_len > 0 {
let gpu_capacity = effects_meta.prefix_sum_buffer.capacity();
if cpu_len > gpu_capacity {
effects_meta
.prefix_sum_buffer
.reserve(cpu_len, &render_device);
effect_bind_groups.particle_slabs.clear();
property_bind_groups.clear(true);
effects_meta.indirect_spawner_bind_group = None;
effects_meta.prefix_sum_bind_group = None;
}
assert!(effects_meta.prefix_sum_buffer.buffer().is_some());
effects_meta
.prefix_sum_buffer
.write_buffer(&render_device, &render_queue);
}
}
}
pub(crate) struct BufferBindGroups {
render: BindGroup,
}
#[derive(Debug, Default, Clone, PartialEq, Eq, Hash)]
struct Material {
layout: TextureLayout,
textures: Vec<AssetId<Image>>,
}
impl Material {
pub fn make_entries<'a>(
&self,
gpu_images: &'a RenderAssets<GpuImage>,
) -> Result<Vec<BindGroupEntry<'a>>, ()> {
if self.textures.is_empty() {
return Ok(vec![]);
}
let entries: Vec<BindGroupEntry<'a>> = self
.textures
.iter()
.enumerate()
.flat_map(|(index, id)| {
let base_binding = index as u32 * 2;
if let Some(gpu_image) = gpu_images.get(*id) {
vec![
BindGroupEntry {
binding: base_binding,
resource: BindingResource::TextureView(&gpu_image.texture_view),
},
BindGroupEntry {
binding: base_binding + 1,
resource: BindingResource::Sampler(&gpu_image.sampler),
},
]
} else {
vec![]
}
})
.collect();
if entries.len() == self.textures.len() * 2 {
return Ok(entries);
}
Err(())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
struct BindingKey {
pub buffer_id: BufferId,
pub offset: u32,
pub size: NonZeroU32,
}
impl<'a> From<BufferSlice<'a>> for BindingKey {
fn from(value: BufferSlice<'a>) -> Self {
Self {
buffer_id: value.buffer.id(),
offset: value.offset,
size: value.size,
}
}
}
impl<'a> From<&BufferSlice<'a>> for BindingKey {
fn from(value: &BufferSlice<'a>) -> Self {
Self {
buffer_id: value.buffer.id(),
offset: value.offset,
size: value.size,
}
}
}
impl From<&BufferBindingSource> for BindingKey {
fn from(value: &BufferBindingSource) -> Self {
Self {
buffer_id: value.buffer.id(),
offset: value.offset,
size: value.size,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
struct ConsumeEventKey {
child_infos_buffer_id: BufferId,
events: BindingKey,
}
impl From<&ConsumeEventBuffers<'_>> for ConsumeEventKey {
fn from(value: &ConsumeEventBuffers) -> Self {
Self {
child_infos_buffer_id: value.child_infos_buffer.id(),
events: value.events.into(),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
struct InitMetadataBindGroupKey {
pub slab_id: SlabId,
pub effect_metadata_buffer: BufferId,
pub consume_event_key: Option<ConsumeEventKey>,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
struct UpdateMetadataBindGroupKey {
pub slab_id: SlabId,
pub effect_metadata_buffer: BufferId,
pub child_info_buffer_id: Option<BufferId>,
pub event_buffers_keys: Vec<BindingKey>,
}
struct CachedBindGroup<K: Eq> {
key: K,
bind_group: BindGroup,
}
#[derive(Debug, Clone, Copy)]
struct BufferSlice<'a> {
pub buffer: &'a Buffer,
pub offset: u32,
pub size: NonZeroU32,
}
impl<'a> From<BufferSlice<'a>> for BufferBinding<'a> {
fn from(value: BufferSlice<'a>) -> Self {
Self {
buffer: value.buffer,
offset: value.offset.into(),
size: Some(value.size.into()),
}
}
}
impl<'a> From<&BufferSlice<'a>> for BufferBinding<'a> {
fn from(value: &BufferSlice<'a>) -> Self {
Self {
buffer: value.buffer,
offset: value.offset.into(),
size: Some(value.size.into()),
}
}
}
impl<'a> From<&'a BufferBindingSource> for BufferSlice<'a> {
fn from(value: &'a BufferBindingSource) -> Self {
Self {
buffer: &value.buffer,
offset: value.offset,
size: value.size,
}
}
}
struct ConsumeEventBuffers<'a> {
child_infos_buffer: &'a Buffer,
events: BufferSlice<'a>,
}
#[derive(Default, Resource)]
pub struct EffectBindGroups {
particle_slabs: HashMap<SlabId, BufferBindGroups>,
images: HashMap<AssetId<Image>, BindGroup>,
init_metadata_bind_groups: HashMap<SlabId, CachedBindGroup<InitMetadataBindGroupKey>>,
update_metadata_bind_groups: HashMap<SlabId, CachedBindGroup<UpdateMetadataBindGroupKey>>,
material_bind_groups: HashMap<Material, BindGroup>,
}
impl EffectBindGroups {
pub fn particle_render(&self, slab_id: &SlabId) -> Option<&BindGroup> {
self.particle_slabs.get(slab_id).map(|bg| &bg.render)
}
pub(self) fn get_or_create_init_metadata(
&mut self,
effect_batch: &EffectBatch,
render_device: &RenderDevice,
layout: &BindGroupLayout,
effect_metadata_buffer: &Buffer,
consume_event_buffers: Option<ConsumeEventBuffers>,
) -> Result<&BindGroup, ()> {
assert!(effect_batch.metadata_table_id.is_valid());
let key = InitMetadataBindGroupKey {
slab_id: effect_batch.slab_id,
effect_metadata_buffer: effect_metadata_buffer.id(),
consume_event_key: consume_event_buffers.as_ref().map(Into::into),
};
let make_entry = || {
let mut entries = Vec::with_capacity(3);
entries.push(
BindGroupEntry {
binding: 0,
resource: effect_metadata_buffer.as_entire_binding(),
},
);
if let Some(consume_event_buffers) = consume_event_buffers.as_ref() {
entries.push(
BindGroupEntry {
binding: 1,
resource: BindingResource::Buffer(BufferBinding {
buffer: consume_event_buffers.child_infos_buffer,
offset: 0,
size: None,
}),
},
);
entries.push(
BindGroupEntry {
binding: 2,
resource: BindingResource::Buffer(consume_event_buffers.events.into()),
},
);
}
let bind_group =
render_device.create_bind_group("hanabi:bg:init:metadata@3", layout, &entries[..]);
trace!(
"Created new metadata@3 bind group for init pass and buffer index {}: effect_metadata=#{}",
effect_batch.slab_id.index(),
effect_batch.metadata_table_id.0,
);
bind_group
};
Ok(&self
.init_metadata_bind_groups
.entry(effect_batch.slab_id)
.and_modify(|cbg| {
if cbg.key != key {
trace!(
"Bind group key changed for init metadata@3, re-creating bind group... old={:?} new={:?}",
cbg.key,
key
);
cbg.key = key;
cbg.bind_group = make_entry();
}
})
.or_insert_with(|| {
trace!("Inserting new bind group for init metadata@3 with key={:?}", key);
CachedBindGroup {
key,
bind_group: make_entry(),
}
})
.bind_group)
}
pub(self) fn get_or_create_update_metadata(
&mut self,
effect_batch: &EffectBatch,
render_device: &RenderDevice,
layout: &BindGroupLayout,
effect_metadata_buffer: &Buffer,
child_info_buffer: Option<&Buffer>,
event_buffers: &[(Entity, BufferBindingSource)],
) -> Result<&BindGroup, ()> {
assert!(effect_batch.metadata_table_id.is_valid());
assert_eq!(effect_batch.child_event_buffers.len(), event_buffers.len());
let emits_gpu_spawn_events = !event_buffers.is_empty();
let child_info_buffer_id = if emits_gpu_spawn_events {
child_info_buffer.as_ref().map(|buffer| buffer.id())
} else {
None
};
assert_eq!(emits_gpu_spawn_events, child_info_buffer_id.is_some());
let event_buffers_keys = event_buffers
.iter()
.map(|(_, buffer_binding_source)| buffer_binding_source.into())
.collect::<Vec<_>>();
let key = UpdateMetadataBindGroupKey {
slab_id: effect_batch.slab_id,
effect_metadata_buffer: effect_metadata_buffer.id(),
child_info_buffer_id,
event_buffers_keys,
};
let make_entry = || {
let mut entries = Vec::with_capacity(2 + event_buffers.len());
entries.push(BindGroupEntry {
binding: 0,
resource: effect_metadata_buffer.as_entire_binding(),
});
if emits_gpu_spawn_events {
let child_info_buffer = child_info_buffer.unwrap();
entries.push(BindGroupEntry {
binding: 1,
resource: BindingResource::Buffer(BufferBinding {
buffer: child_info_buffer,
offset: 0,
size: None,
}),
});
for (index, (_, buffer_binding_source)) in event_buffers.iter().enumerate() {
let mut buffer_binding: BufferBinding = buffer_binding_source.into();
buffer_binding.offset *= 4;
buffer_binding.size = buffer_binding
.size
.map(|sz| NonZeroU64::new(sz.get() * 4).unwrap());
entries.push(BindGroupEntry {
binding: 2 + index as u32,
resource: BindingResource::Buffer(buffer_binding),
});
}
}
let bind_group = render_device.create_bind_group(
"hanabi:bg:update:metadata@3",
layout,
&entries[..],
);
trace!(
"Created new metadata@3 bind group for update pass and slab ID {}: effect_metadata={}",
effect_batch.slab_id.index(),
effect_batch.metadata_table_id.0,
);
bind_group
};
Ok(&self
.update_metadata_bind_groups
.entry(effect_batch.slab_id)
.and_modify(|cbg| {
if cbg.key != key {
trace!(
"Bind group key changed for update metadata@3, re-creating bind group... old={:?} new={:?}",
cbg.key,
key
);
cbg.key = key.clone();
cbg.bind_group = make_entry();
}
})
.or_insert_with(|| {
trace!(
"Inserting new bind group for update metadata@3 with key={:?}",
key
);
CachedBindGroup {
key: key.clone(),
bind_group: make_entry(),
}
})
.bind_group)
}
}
#[derive(SystemParam)]
pub struct QueueEffectsReadOnlyParams<'w, 's> {
effects_meta: Res<'w, EffectsMeta>,
property_cache: Res<'w, PropertyCache>,
pipeline_cache: Res<'w, PipelineCache>,
#[cfg(feature = "2d")]
draw_functions_2d: Res<'w, DrawFunctions<Transparent2d>>,
#[cfg(feature = "3d")]
draw_functions_3d: Res<'w, DrawFunctions<Transparent3d>>,
#[cfg(feature = "3d")]
draw_functions_alpha_mask: Res<'w, DrawFunctions<AlphaMask3d>>,
#[cfg(feature = "3d")]
draw_functions_opaque: Res<'w, DrawFunctions<Opaque3d>>,
marker: PhantomData<&'s usize>,
}
fn emit_sorted_draw<T, F>(
views: &Query<(&RenderVisibleEntities, &ExtractedView, &Msaa)>,
render_phases: &mut ResMut<ViewSortedRenderPhases<T>>,
view_entities: &mut FixedBitSet,
sorted_effect_batches: &SortedEffectBatches,
effect_draw_batches: &Query<(Entity, &mut EffectDrawBatch)>,
render_pipeline: &mut ParticlesRenderPipeline,
mut specialized_render_pipelines: Mut<SpecializedRenderPipelines<ParticlesRenderPipeline>>,
property_cache: &PropertyCache,
render_meshes: &RenderAssets<RenderMesh>,
pipeline_cache: &PipelineCache,
make_phase_item: F,
#[cfg(all(feature = "2d", feature = "3d"))] pipeline_mode: PipelineMode,
) where
T: SortedPhaseItem,
F: Fn(CachedRenderPipelineId, (Entity, MainEntity), &EffectDrawBatch, &ExtractedView) -> T,
{
trace!("emit_sorted_draw() {} views", views.iter().len());
for (visible_entities, view, msaa) in views.iter() {
let Some(visible_effect_entities) = visible_entities.get::<EffectVisibilityClass>() else {
continue;
};
trace!(
"Process new sorted view with {} visible particle effect entities",
visible_effect_entities.iter_visible().count()
);
let Some(render_phase) = render_phases.get_mut(&view.retained_view_entity) else {
continue;
};
{
#[cfg(feature = "trace")]
let _span = bevy::log::info_span!("collect_view_entities").entered();
view_entities.clear();
view_entities.extend(
visible_effect_entities
.iter_visible()
.map(|(_, main_entity)| main_entity.index_u32() as usize),
);
}
for (draw_entity, draw_batch) in effect_draw_batches.iter() {
#[cfg(feature = "trace")]
let _span_draw = bevy::log::info_span!("draw_batch").entered();
trace!(
"Process draw batch: draw_entity={:?} effect_batch_index={:?}",
draw_entity,
draw_batch.effect_batch_index,
);
let Some(effect_batch) = sorted_effect_batches.get(draw_batch.effect_batch_index)
else {
continue;
};
trace!(
"-> EffectBach: slab_id={} spawner_base={} layout_flags={:?}",
effect_batch.slab_id.index(),
effect_batch.spawner_base,
effect_batch.layout_flags,
);
if effect_batch
.layout_flags
.intersects(LayoutFlags::USE_ALPHA_MASK | LayoutFlags::OPAQUE)
{
trace!("Non-transparent batch. Skipped.");
continue;
}
#[cfg(feature = "trace")]
let _span_check_vis = bevy::log::info_span!("check_visibility").entered();
let has_visible_entity = effect_batch
.entities
.iter()
.any(|index| view_entities.contains(*index as usize));
if !has_visible_entity {
trace!("No visible entity for view, not emitting any draw call.");
continue;
}
#[cfg(feature = "trace")]
_span_check_vis.exit();
render_pipeline.cache_material(&effect_batch.texture_layout);
let local_space_simulation = effect_batch
.layout_flags
.contains(LayoutFlags::LOCAL_SPACE_SIMULATION);
let alpha_mask = ParticleRenderAlphaMaskPipelineKey::from(effect_batch.layout_flags);
let flipbook = effect_batch.layout_flags.contains(LayoutFlags::FLIPBOOK);
let needs_uv = effect_batch.layout_flags.contains(LayoutFlags::NEEDS_UV);
let needs_normal = effect_batch
.layout_flags
.contains(LayoutFlags::NEEDS_NORMAL);
let needs_particle_fragment = effect_batch
.layout_flags
.contains(LayoutFlags::NEEDS_PARTICLE_FRAGMENT);
let ribbons = effect_batch.layout_flags.contains(LayoutFlags::RIBBONS);
let image_count = effect_batch.texture_layout.layout.len() as u8;
let Some(render_mesh) = render_meshes.get(effect_batch.mesh) else {
trace!("Batch has no render mesh, skipped.");
continue;
};
let mesh_layout = render_mesh.layout.clone();
trace!(
"Specializing render pipeline: render_shader={:?} image_count={} alpha_mask={:?} flipbook={:?} target_format={:?}",
effect_batch.render_shader,
image_count,
alpha_mask,
flipbook,
view.target_format
);
trace!("Emitting individual draw for batch");
let alpha_mode = effect_batch.alpha_mode;
let property_layout_min_binding_size = effect_batch
.property_key
.map(|key| NonZeroU64::new(key.binding_size as u64).unwrap());
let spawner_bind_group_layout_desc = property_cache
.bind_group_layout_desc(property_layout_min_binding_size)
.unwrap_or_else(|| {
panic!(
"Failed to find spawner@2 bind group layout for property binding size {:?}",
property_layout_min_binding_size,
)
});
#[cfg(feature = "trace")]
let _span_specialize = bevy::log::info_span!("specialize").entered();
let render_pipeline_id = specialized_render_pipelines.specialize(
pipeline_cache,
render_pipeline,
ParticleRenderPipelineKey {
shader: effect_batch.render_shader.clone(),
mesh_layout: Some(mesh_layout),
particle_layout: effect_batch.particle_layout.clone(),
texture_layout: effect_batch.texture_layout.clone(),
local_space_simulation,
alpha_mask,
alpha_mode,
flipbook,
needs_uv,
needs_normal,
needs_particle_fragment,
ribbons,
#[cfg(all(feature = "2d", feature = "3d"))]
pipeline_mode,
msaa_samples: msaa.samples(),
target_format: view.target_format,
spawner_bind_group_layout_desc: spawner_bind_group_layout_desc.clone(),
},
);
#[cfg(feature = "trace")]
_span_specialize.exit();
trace!("+ Render pipeline specialized: id={:?}", render_pipeline_id,);
trace!(
"+ Add Transparent for batch on draw_entity {:?}: slab_id={} \
spawner_base={} handle={:?}",
draw_entity,
effect_batch.slab_id.index(),
effect_batch.spawner_base,
effect_batch.handle
);
render_phase.add_transient(make_phase_item(
render_pipeline_id,
(draw_entity, draw_batch.main_entity),
draw_batch,
view,
));
}
}
}
#[cfg(feature = "3d")]
fn emit_binned_draw<T, F, G>(
views: &Query<(&RenderVisibleEntities, &ExtractedView, &Msaa)>,
render_phases: &mut ResMut<ViewBinnedRenderPhases<T>>,
view_entities: &mut FixedBitSet,
sorted_effect_batches: &SortedEffectBatches,
effect_draw_batches: &Query<(Entity, &mut EffectDrawBatch)>,
render_pipeline: &mut ParticlesRenderPipeline,
mut specialized_render_pipelines: Mut<SpecializedRenderPipelines<ParticlesRenderPipeline>>,
property_cache: &PropertyCache,
pipeline_cache: &PipelineCache,
render_meshes: &RenderAssets<RenderMesh>,
mesh_allocator: &MeshAllocator,
make_batch_set_key: F,
make_bin_key: G,
#[cfg(all(feature = "2d", feature = "3d"))] pipeline_mode: PipelineMode,
alpha_mask: ParticleRenderAlphaMaskPipelineKey,
) where
T: BinnedPhaseItem,
F: Fn(CachedRenderPipelineId, &EffectDrawBatch, &ExtractedView, MeshSlabs) -> T::BatchSetKey,
G: Fn() -> T::BinKey,
{
use bevy::render::render_phase::{BinnedRenderPhaseType, InputUniformIndex};
trace!("emit_binned_draw() {} views", views.iter().len());
for (visible_entities, view, msaa) in views.iter() {
let Some(visible_effect_entities) = visible_entities.get::<EffectVisibilityClass>() else {
continue;
};
trace!("Process new binned view (alpha_mask={:?})", alpha_mask);
let Some(render_phase) = render_phases.get_mut(&view.retained_view_entity) else {
continue;
};
{
#[cfg(feature = "trace")]
let _span = bevy::log::info_span!("collect_view_entities").entered();
view_entities.clear();
view_entities.extend(
visible_effect_entities
.iter_visible()
.map(|(_, main_entity)| main_entity.index_u32() as usize),
);
}
for (draw_entity, draw_batch) in effect_draw_batches.iter() {
#[cfg(feature = "trace")]
let _span_draw = bevy::log::info_span!("draw_batch").entered();
trace!(
"Process draw batch: draw_entity={:?} effect_batch_index={:?}",
draw_entity,
draw_batch.effect_batch_index,
);
let Some(effect_batch) = sorted_effect_batches.get(draw_batch.effect_batch_index)
else {
continue;
};
trace!(
"-> EffectBaches: slab_id={} spawner_base={} layout_flags={:?}",
effect_batch.slab_id.index(),
effect_batch.spawner_base,
effect_batch.layout_flags,
);
if ParticleRenderAlphaMaskPipelineKey::from(effect_batch.layout_flags) != alpha_mask {
trace!(
"Mismatching alpha mask pipeline key (batches={:?}, expected={:?}). Skipped.",
effect_batch.layout_flags,
alpha_mask
);
continue;
}
#[cfg(feature = "trace")]
let _span_check_vis = bevy::log::info_span!("check_visibility").entered();
let has_visible_entity = effect_batch
.entities
.iter()
.any(|index| view_entities.contains(*index as usize));
if !has_visible_entity {
trace!("No visible entity for view, not emitting any draw call.");
continue;
}
#[cfg(feature = "trace")]
_span_check_vis.exit();
render_pipeline.cache_material(&effect_batch.texture_layout);
let local_space_simulation = effect_batch
.layout_flags
.contains(LayoutFlags::LOCAL_SPACE_SIMULATION);
let alpha_mask = ParticleRenderAlphaMaskPipelineKey::from(effect_batch.layout_flags);
let flipbook = effect_batch.layout_flags.contains(LayoutFlags::FLIPBOOK);
let needs_uv = effect_batch.layout_flags.contains(LayoutFlags::NEEDS_UV);
let needs_normal = effect_batch
.layout_flags
.contains(LayoutFlags::NEEDS_NORMAL);
let needs_particle_fragment = effect_batch
.layout_flags
.contains(LayoutFlags::NEEDS_PARTICLE_FRAGMENT);
let ribbons = effect_batch.layout_flags.contains(LayoutFlags::RIBBONS);
let image_count = effect_batch.texture_layout.layout.len() as u8;
let render_mesh = render_meshes.get(effect_batch.mesh);
trace!(
"Specializing render pipeline: render_shaders={:?} image_count={} alpha_mask={:?} flipbook={:?} target_format={:?}",
effect_batch.render_shader,
image_count,
alpha_mask,
flipbook,
view.target_format
);
trace!("Emitting individual draw for batch");
let alpha_mode = effect_batch.alpha_mode;
let Some(mesh_layout) = render_mesh.map(|gpu_mesh| gpu_mesh.layout.clone()) else {
trace!("Missing mesh vertex buffer layout. Skipped.");
continue;
};
let property_layout_min_binding_size = effect_batch
.property_key
.map(|key| NonZeroU64::new(key.binding_size as u64).unwrap());
let spawner_bind_group_layout_desc = property_cache
.bind_group_layout_desc(property_layout_min_binding_size)
.unwrap_or_else(|| {
panic!(
"Failed to find spawner@2 bind group layout for property binding size {:?}",
property_layout_min_binding_size,
)
});
#[cfg(feature = "trace")]
let _span_specialize = bevy::log::info_span!("specialize").entered();
let render_pipeline_id = specialized_render_pipelines.specialize(
pipeline_cache,
render_pipeline,
ParticleRenderPipelineKey {
shader: effect_batch.render_shader.clone(),
mesh_layout: Some(mesh_layout),
particle_layout: effect_batch.particle_layout.clone(),
texture_layout: effect_batch.texture_layout.clone(),
local_space_simulation,
alpha_mask,
alpha_mode,
flipbook,
needs_uv,
needs_normal,
needs_particle_fragment,
ribbons,
#[cfg(all(feature = "2d", feature = "3d"))]
pipeline_mode,
msaa_samples: msaa.samples(),
target_format: view.target_format,
spawner_bind_group_layout_desc: spawner_bind_group_layout_desc.clone(),
},
);
#[cfg(feature = "trace")]
_span_specialize.exit();
trace!("+ Render pipeline specialized: id={:?}", render_pipeline_id,);
trace!(
"+ Add Transparent for batch on draw_entity {:?}: slab_id={} \
spawner_base={} handle={:?}",
draw_entity,
effect_batch.slab_id.index(),
effect_batch.spawner_base,
effect_batch.handle
);
let slabs = mesh_allocator
.mesh_slabs(&effect_batch.mesh)
.unwrap_or_default();
render_phase.add(
make_batch_set_key(render_pipeline_id, draw_batch, view, slabs),
make_bin_key(),
(draw_entity, draw_batch.main_entity),
InputUniformIndex::default(),
BinnedRenderPhaseType::NonMesh,
);
}
}
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn queue_effects(
views: Query<(&RenderVisibleEntities, &ExtractedView, &Msaa)>,
mut render_pipeline: ResMut<ParticlesRenderPipeline>,
mut specialized_render_pipelines: ResMut<SpecializedRenderPipelines<ParticlesRenderPipeline>>,
mut effect_bind_groups: ResMut<EffectBindGroups>,
sorted_effect_batches: Res<SortedEffectBatches>,
effect_draw_batches: Query<(Entity, &mut EffectDrawBatch)>,
events: Res<EffectAssetEvents>,
render_meshes: Res<RenderAssets<RenderMesh>>,
#[cfg(feature = "3d")] mesh_allocator: Res<MeshAllocator>,
read_params: QueueEffectsReadOnlyParams,
mut view_entities: Local<FixedBitSet>,
#[cfg(feature = "2d")] mut transparent_2d_render_phases: ResMut<
ViewSortedRenderPhases<Transparent2d>,
>,
#[cfg(feature = "3d")] mut transparent_3d_render_phases: ResMut<
ViewSortedRenderPhases<Transparent3d>,
>,
#[cfg(feature = "3d")] (mut opaque_3d_render_phases, mut alpha_mask_3d_render_phases): (
ResMut<ViewBinnedRenderPhases<Opaque3d>>,
ResMut<ViewBinnedRenderPhases<AlphaMask3d>>,
),
mut change_tick: Local<Tick>,
) {
#[cfg(feature = "trace")]
let _span = bevy::log::info_span!("hanabi:queue_effects").entered();
trace!("queue_effects");
let effects_meta = read_params.effects_meta.into_inner();
let property_cache = read_params.property_cache.into_inner();
let pipeline_cache = read_params.pipeline_cache.into_inner();
let next_change_tick = change_tick.get() + 1;
change_tick.set(next_change_tick);
for event in &events.images {
match event {
AssetEvent::Added { .. } => (),
AssetEvent::LoadedWithDependencies { .. } => (),
AssetEvent::Unused { .. } => (),
AssetEvent::Modified { id } => {
if effect_bind_groups.images.remove(id).is_some() {
trace!("Destroyed bind group of modified image asset {:?}", id);
}
}
AssetEvent::Removed { id } => {
if effect_bind_groups.images.remove(id).is_some() {
trace!("Destroyes bind group of removed image asset {:?}", id);
}
}
};
}
if effects_meta.spawner_buffer.buffer().is_none() || effects_meta.spawner_buffer.is_empty() {
return;
}
#[cfg(feature = "2d")]
{
#[cfg(feature = "trace")]
let _span_draw = bevy::log::info_span!("draw_2d").entered();
let draw_effects_function_2d = read_params
.draw_functions_2d
.read()
.get_id::<DrawEffects>()
.unwrap();
if !views.is_empty() {
trace!("Emit effect draw calls for alpha blended 2D views...");
emit_sorted_draw(
&views,
&mut transparent_2d_render_phases,
&mut view_entities,
&sorted_effect_batches,
&effect_draw_batches,
&mut render_pipeline,
specialized_render_pipelines.reborrow(),
property_cache,
&render_meshes,
pipeline_cache,
|id, entity, draw_batch, _view| Transparent2d {
sort_key: FloatOrd(draw_batch.translation.z),
entity,
pipeline: id,
draw_function: draw_effects_function_2d,
batch_range: 0..1,
extracted_index: 0, extra_index: PhaseItemExtraIndex::None,
indexed: true, },
#[cfg(feature = "3d")]
PipelineMode::Camera2d,
);
}
}
#[cfg(feature = "3d")]
{
#[cfg(feature = "trace")]
let _span_draw = bevy::log::info_span!("draw_3d").entered();
if !views.is_empty() {
use bevy::core_pipeline::core_3d::TransparentSortingInfo3d;
trace!("Emit effect draw calls for alpha blended 3D views...");
let draw_effects_function_3d = read_params
.draw_functions_3d
.read()
.get_id::<DrawEffects>()
.unwrap();
emit_sorted_draw(
&views,
&mut transparent_3d_render_phases,
&mut view_entities,
&sorted_effect_batches,
&effect_draw_batches,
&mut render_pipeline,
specialized_render_pipelines.reborrow(),
property_cache,
&render_meshes,
pipeline_cache,
|id, entity, batch, view| Transparent3d {
sorting_info: TransparentSortingInfo3d::Sorted {
mesh_center: batch.translation,
depth_bias: 0.0,
},
distance: view.rangefinder3d().distance(&batch.translation),
pipeline: id,
entity,
draw_function: draw_effects_function_3d,
batch_range: 0..1,
extra_index: PhaseItemExtraIndex::None,
indexed: true, },
#[cfg(feature = "2d")]
PipelineMode::Camera3d,
);
}
if !views.is_empty() {
#[cfg(feature = "trace")]
let _span_draw = bevy::log::info_span!("draw_alphamask").entered();
trace!("Emit effect draw calls for alpha masked 3D views...");
let draw_effects_function_alpha_mask = read_params
.draw_functions_alpha_mask
.read()
.get_id::<DrawEffects>()
.unwrap();
emit_binned_draw(
&views,
&mut alpha_mask_3d_render_phases,
&mut view_entities,
&sorted_effect_batches,
&effect_draw_batches,
&mut render_pipeline,
specialized_render_pipelines.reborrow(),
property_cache,
pipeline_cache,
&render_meshes,
&mesh_allocator,
|id, _batch, _view, slabs| OpaqueNoLightmap3dBatchSetKey {
pipeline: id,
draw_function: draw_effects_function_alpha_mask,
material_bind_group_index: None,
slabs,
},
|| OpaqueNoLightmap3dBinKey {
asset_id: AssetId::<Mesh>::invalid().untyped(),
},
#[cfg(feature = "2d")]
PipelineMode::Camera3d,
ParticleRenderAlphaMaskPipelineKey::AlphaMask,
);
}
if !views.is_empty() {
#[cfg(feature = "trace")]
let _span_draw = bevy::log::info_span!("draw_opaque").entered();
trace!("Emit effect draw calls for opaque 3D views...");
let draw_effects_function_opaque = read_params
.draw_functions_opaque
.read()
.get_id::<DrawEffects>()
.unwrap();
emit_binned_draw(
&views,
&mut opaque_3d_render_phases,
&mut view_entities,
&sorted_effect_batches,
&effect_draw_batches,
&mut render_pipeline,
specialized_render_pipelines.reborrow(),
property_cache,
pipeline_cache,
&render_meshes,
&mesh_allocator,
|id, _batch, _view, slabs| Opaque3dBatchSetKey {
pipeline: id,
draw_function: draw_effects_function_opaque,
material_bind_group_index: None,
slabs,
lightmap_slab: None,
},
|| Opaque3dBinKey {
asset_id: AssetId::<Mesh>::invalid().untyped(),
},
#[cfg(feature = "2d")]
PipelineMode::Camera3d,
ParticleRenderAlphaMaskPipelineKey::Opaque,
);
}
}
}
pub fn queue_init_indirect_workgroup_update(
q_cached_effects: Query<(
Entity,
&CachedChildInfo,
&CachedEffectEvents,
&CachedReadyState,
)>,
mut init_fill_dispatch_queue: ResMut<InitFillDispatchQueue>,
) {
debug_assert_eq!(
GpuChildInfo::min_size().get() % 4,
0,
"Invalid GpuChildInfo alignment."
);
for (entity, cached_child_info, cached_effect_events, cached_ready_state) in &q_cached_effects {
if !cached_ready_state.is_ready() {
trace!(
"[Effect {:?}] Skipping init_fill_dispatch.enqueue() because effect is not ready.",
entity
);
continue;
}
let init_indirect_dispatch_index = cached_effect_events.init_indirect_dispatch_index;
let global_child_index = cached_child_info.global_child_index;
trace!(
"[Effect {:?}] init_fill_dispatch.enqueue(): src:global_child_index={} dst:init_indirect_dispatch_index={}",
entity,
global_child_index,
init_indirect_dispatch_index,
);
assert!(global_child_index != u32::MAX);
init_fill_dispatch_queue.enqueue(global_child_index, init_indirect_dispatch_index);
}
}
pub(crate) fn prepare_gpu_resources(
mut effects_meta: ResMut<EffectsMeta>,
mut event_cache: ResMut<EventCache>,
mut effect_bind_groups: ResMut<EffectBindGroups>,
mut sort_bind_groups: ResMut<SortBindGroups>,
render_device: Res<RenderDevice>,
render_queue: Res<RenderQueue>,
view_uniforms: Res<ViewUniforms>,
render_pipeline: Res<ParticlesRenderPipeline>,
pipeline_cache: Res<PipelineCache>,
) {
let Some(view_binding) = view_uniforms.uniforms.binding() else {
return;
};
let prev_buffer_id = effects_meta.sim_params_uniforms.buffer().map(|b| b.id());
effects_meta
.sim_params_uniforms
.write_buffer(&render_device, &render_queue);
if prev_buffer_id != effects_meta.sim_params_uniforms.buffer().map(|b| b.id()) {
effects_meta.update_sim_params_bind_group = None;
effects_meta.init_and_indirect_sim_params_bind_group = None;
}
effects_meta.view_bind_group = Some(render_device.create_bind_group(
"hanabi:bg:camera_view",
&pipeline_cache.get_bind_group_layout(&render_pipeline.view_layout_desc),
&BindGroupEntries::sequential((
view_binding,
effects_meta.sim_params_uniforms.binding().unwrap(),
)),
));
if effects_meta
.draw_indirect_buffer
.allocate_gpu(&render_device, &render_queue)
{
trace!("*** Draw indirect args buffer re-allocated; clearing all bind groups using it.");
effects_meta.update_sim_params_bind_group = None;
effects_meta.indirect_metadata_bind_group = None;
}
event_cache.prepare_buffers(&render_device, &render_queue, &mut effect_bind_groups);
sort_bind_groups.prepare_buffers(&render_device);
if effects_meta
.dispatch_indirect_buffer
.prepare_buffers(&render_device)
{
trace!("*** Dispatch indirect buffer for update pass re-allocated; clearing all bind groups using it.");
effect_bind_groups.particle_slabs.clear();
}
}
pub(crate) fn prepare_effect_metadata(
render_device: Res<RenderDevice>,
render_queue: Res<RenderQueue>,
mut q_effects: Query<(
MainEntity,
Ref<ExtractedEffect>,
Ref<CachedEffect>,
Option<Ref<CachedChildInfo>>,
Option<Ref<CachedParentInfo>>,
Option<Ref<CachedDrawIndirectArgs>>,
Option<Ref<CachedEffectEvents>>,
Option<Ref<CachedEffectProperties>>,
&mut CachedEffectMetadata,
)>,
mut effects_meta: ResMut<EffectsMeta>,
mut effect_bind_groups: ResMut<EffectBindGroups>,
) {
#[cfg(feature = "trace")]
let _span = bevy::log::info_span!("prepare_effect_metadata").entered();
trace!("prepare_effect_metadata");
for (
main_entity,
extracted_effect,
cached_effect,
maybe_cached_child_info,
maybe_cached_parent_info,
maybe_cached_draw_indirect_args,
maybe_cached_effect_events,
maybe_cached_effect_properties,
mut cached_effect_metadata,
) in &mut q_effects
{
let is_changed_ee = extracted_effect.is_changed();
let is_changed_ce = cached_effect.is_changed();
let is_changed_cci = maybe_cached_child_info
.as_ref()
.map(|cci| cci.is_changed())
.unwrap_or(false);
let is_changed_cpi = maybe_cached_parent_info
.as_ref()
.map(|cpi| cpi.is_changed())
.unwrap_or(false);
let is_changed_cdia = maybe_cached_draw_indirect_args
.as_ref()
.map(|cdia| cdia.is_changed())
.unwrap_or(false);
let is_changed_cee = maybe_cached_effect_events
.as_ref()
.map(|cee| cee.is_changed())
.unwrap_or(false);
let is_changed_cep = maybe_cached_effect_properties
.as_ref()
.map(|cep| cep.is_changed())
.unwrap_or(false);
trace!(
"Preparting GpuEffectMetadata for effect {:?}: is_changed[] = {} {} {} {} {} {} {}",
main_entity,
is_changed_ee,
is_changed_ce,
is_changed_cci,
is_changed_cpi,
is_changed_cdia,
is_changed_cee,
is_changed_cep
);
if !is_changed_ee
&& !is_changed_ce
&& !is_changed_cci
&& !is_changed_cpi
&& !is_changed_cdia
&& !is_changed_cee
&& !is_changed_cep
{
continue;
}
let capacity = cached_effect.slice.len();
let (global_child_index, local_child_index) = maybe_cached_child_info
.map(|cci| (cci.global_child_index, cci.local_child_index))
.unwrap_or((u32::MAX, u32::MAX));
let base_child_index = maybe_cached_parent_info
.map(|cpi| {
debug_assert_eq!(
cpi.byte_range.start % GpuChildInfo::SHADER_SIZE.get() as u32,
0
);
cpi.byte_range.start / GpuChildInfo::SHADER_SIZE.get() as u32
})
.unwrap_or(u32::MAX);
let particle_stride = extracted_effect.particle_layout.min_binding_size32().get() / 4;
let sort_key_offset = extracted_effect
.particle_layout
.byte_offset(Attribute::RIBBON_ID)
.map(|byte_offset| byte_offset / 4)
.unwrap_or(u32::MAX);
let sort_key2_offset = extracted_effect
.particle_layout
.byte_offset(Attribute::AGE)
.map(|byte_offset| byte_offset / 4)
.unwrap_or(u32::MAX);
let gpu_effect_metadata = GpuEffectMetadata {
capacity,
alive_count: 0,
max_update: 0,
max_spawn: capacity,
indirect_write_index: 0,
indirect_draw_index: maybe_cached_draw_indirect_args
.map(|cdia| cdia.get_row().0)
.unwrap_or(u32::MAX),
init_indirect_dispatch_index: maybe_cached_effect_events
.map(|cee| cee.init_indirect_dispatch_index)
.unwrap_or(u32::MAX),
properties_array_index: maybe_cached_effect_properties
.map(|cep| cep.array_index)
.unwrap_or(u32::MAX),
local_child_index,
global_child_index,
base_child_index,
particle_stride,
sort_key_offset,
sort_key2_offset,
..default()
};
assert!(cached_effect_metadata.table_id.is_valid());
if gpu_effect_metadata != cached_effect_metadata.metadata {
effects_meta
.effect_metadata_buffer
.update(cached_effect_metadata.table_id, gpu_effect_metadata);
cached_effect_metadata.metadata = gpu_effect_metadata;
debug!(
"Updated metadata entry {} for effect {:?}, this will reset it.",
cached_effect_metadata.table_id.0, main_entity
);
}
}
if effects_meta
.effect_metadata_buffer
.allocate_gpu(render_device.as_ref(), render_queue.as_ref())
{
trace!("*** Effect metadata buffer re-allocated; clearing all bind groups using it.");
effects_meta.indirect_metadata_bind_group = None;
effect_bind_groups.init_metadata_bind_groups.clear();
effect_bind_groups.update_metadata_bind_groups.clear();
}
}
pub(crate) fn queue_sort_fill_dispatch_ops(
effects_meta: Res<EffectsMeta>,
sort_bind_groups: Res<SortBindGroups>,
sort_fill_dispatch_queue: Res<SortFillDispatchQueue>,
mut gpu_buffer_operations: ResMut<GpuBufferOperations>,
mut sorted_effect_batches: ResMut<SortedEffectBatches>,
) {
#[cfg(feature = "trace")]
let _span = bevy::log::info_span!("queue_sort_fill_dispatch_ops").entered();
trace!("queue_sort_fill_dispatch_ops");
debug_assert!(sorted_effect_batches.dispatch_queue_index.is_none());
sorted_effect_batches.dispatch_queue_index = sort_fill_dispatch_queue.submit(
&effects_meta.effect_metadata_buffer,
effects_meta.gpu_limits.effect_metadata_aligned_size,
&sort_bind_groups,
&mut gpu_buffer_operations,
);
}
pub(crate) fn queue_init_fill_dispatch_ops(
event_cache: Res<EventCache>,
render_device: Res<RenderDevice>,
render_queue: Res<RenderQueue>,
mut init_fill_dispatch_queue: ResMut<InitFillDispatchQueue>,
mut gpu_buffer_operations: ResMut<GpuBufferOperations>,
) {
if !init_fill_dispatch_queue.is_empty() {
let src_buffer = event_cache.child_infos().buffer();
let dst_buffer = event_cache.init_indirect_dispatch_buffer();
if let (Some(src_buffer), Some(dst_buffer)) = (src_buffer, dst_buffer) {
init_fill_dispatch_queue.submit(src_buffer, dst_buffer, &mut gpu_buffer_operations);
} else {
if src_buffer.is_none() {
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());
}
if dst_buffer.is_none() {
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());
}
}
}
gpu_buffer_operations.end_frame(&render_device, &render_queue);
}
#[derive(SystemParam)]
pub struct PipelineParams<'w, 's> {
dispatch_indirect_pipeline: Res<'w, DispatchIndirectPipeline>,
prefix_sum_pipeline: Res<'w, PrefixSumPipeline>,
utils_pipeline: Res<'w, UtilsPipeline>,
init_pipeline: Res<'w, ParticlesInitPipeline>,
update_pipeline: Res<'w, ParticlesUpdatePipeline>,
render_pipeline: ResMut<'w, ParticlesRenderPipeline>,
marker: PhantomData<&'s usize>,
}
pub(crate) fn prepare_bind_groups(
mut effects_meta: ResMut<EffectsMeta>,
mut effect_cache: ResMut<EffectCache>,
mut event_cache: ResMut<EventCache>,
mut effect_bind_groups: ResMut<EffectBindGroups>,
mut property_bind_groups: ResMut<PropertyBindGroups>,
mut sort_bind_groups: ResMut<SortBindGroups>,
property_cache: Res<PropertyCache>,
sorted_effect_batched: Res<SortedEffectBatches>,
render_device: Res<RenderDevice>,
pipeline_cache: Res<PipelineCache>,
pipelines: PipelineParams,
gpu_images: Res<RenderAssets<GpuImage>>,
mut gpu_buffer_operation_queue: ResMut<GpuBufferOperations>,
) {
if effects_meta.spawner_buffer.is_empty() {
return;
}
let Some(spawner_buffer) = effects_meta.spawner_buffer.buffer().cloned() else {
error!("Missing spawner GPU buffer!");
return;
};
let Some(prefix_sum_buffer) = effects_meta.prefix_sum_buffer.buffer().cloned() else {
error!("Missing prefix sum GPU buffer!");
return;
};
let Some(batch_info_buffer) = effects_meta.batch_info_buffer.buffer().cloned() else {
error!("Missing batch info GPU buffer!");
return;
};
let Some(dispatch_indirect_buffer) = effects_meta.dispatch_indirect_buffer.buffer().cloned()
else {
error!("Missing dispatch indirect GPU buffer!");
return;
};
let dispatch_indirect_pipeline = pipelines.dispatch_indirect_pipeline.into_inner();
let prefix_sum_pipeline = pipelines.prefix_sum_pipeline.into_inner();
let utils_pipeline = pipelines.utils_pipeline.into_inner();
let init_pipeline = pipelines.init_pipeline.into_inner();
let update_pipeline = pipelines.update_pipeline.into_inner();
let render_pipeline = pipelines.render_pipeline.into_inner();
event_cache.ensure_indirect_child_info_buffer_bind_group(&render_device);
{
#[cfg(feature = "trace")]
let _span = bevy::log::info_span!("shared_bind_groups").entered();
if effects_meta.update_sim_params_bind_group.is_none() {
if let Some(draw_indirect_buffer) = effects_meta.draw_indirect_buffer.buffer() {
effects_meta.update_sim_params_bind_group = Some(render_device.create_bind_group(
"hanabi:bg:vfx_update:sim_params@0",
&pipeline_cache.get_bind_group_layout(&update_pipeline.sim_params_layout_desc),
&BindGroupEntries::sequential((
effects_meta.sim_params_uniforms.binding().unwrap(),
draw_indirect_buffer.as_entire_binding(),
)),
));
} else {
debug!("Cannot allocate bind group for vfx_update:sim_params@0 - draw_indirect_buffer not ready");
}
}
if effects_meta
.init_and_indirect_sim_params_bind_group
.is_none()
{
debug!("Re-creating init_and_indirect_sim_params_bind_group...");
effects_meta.init_and_indirect_sim_params_bind_group =
Some(render_device.create_bind_group(
"hanabi:bg:vfx_indirect:sim_params@0",
&pipeline_cache.get_bind_group_layout(&init_pipeline.sim_params_layout_desc), &BindGroupEntries::single(effects_meta.sim_params_uniforms.binding().unwrap()),
));
}
effects_meta.indirect_metadata_bind_group = match (
effects_meta.effect_metadata_buffer.buffer(),
effects_meta.draw_indirect_buffer.buffer(),
) {
(Some(effect_metadata_buffer), Some(draw_indirect_buffer)) => {
Some(render_device.create_bind_group(
"hanabi:bg:vfx_indirect:metadata@1",
&pipeline_cache.get_bind_group_layout(
&dispatch_indirect_pipeline.effect_metadata_bind_group_layout_desc,
),
&BindGroupEntries::sequential((
effect_metadata_buffer.as_entire_binding(),
dispatch_indirect_buffer.as_entire_binding(),
draw_indirect_buffer.as_entire_binding(),
)),
))
}
_ => None,
};
if effects_meta.indirect_spawner_bind_group.is_none() {
let bind_group = render_device.create_bind_group(
"hanabi:bg:vfx_indirect:spawner@2",
&pipeline_cache.get_bind_group_layout(
&dispatch_indirect_pipeline.spawner_bind_group_layout_desc,
),
&BindGroupEntries::sequential((
spawner_buffer.as_entire_binding(),
prefix_sum_buffer.as_entire_binding(),
)),
);
effects_meta.indirect_spawner_bind_group = Some(bind_group);
}
if effects_meta.prefix_sum_bind_group.is_none() {
effects_meta.prefix_sum_bind_group = Some(render_device.create_bind_group(
"hanabi:bind_group:vfx_prefix_sum:@0",
&pipeline_cache.get_bind_group_layout(&prefix_sum_pipeline.bind_group_layout_desc),
&BindGroupEntries::sequential((
batch_info_buffer.as_entire_binding(),
prefix_sum_buffer.as_entire_binding(),
dispatch_indirect_buffer.as_entire_binding(),
)),
));
}
}
trace!("Create per-slab bind groups...");
for (slab_index, particle_slab) in effect_cache.slabs().iter().enumerate() {
#[cfg(feature = "trace")]
let _span_buffer = bevy::log::info_span!("create_buffer_bind_groups").entered();
let Some(particle_slab) = particle_slab else {
trace!(
"Particle slab index #{} has no allocated EffectBuffer, skipped.",
slab_index
);
continue;
};
trace!("effect particle slab_index=#{}", slab_index);
effect_bind_groups
.particle_slabs
.entry(SlabId::new(slab_index as u32))
.or_insert_with(|| {
trace!("Creating particle@1 bind group for buffer #{slab_index} in render pass");
let render = render_device.create_bind_group(
&format!("hanabi:bg:render:particles@1:vfx{slab_index}")[..],
particle_slab.render_particles_buffer_layout(),
&BindGroupEntries::sequential((
particle_slab.as_entire_binding_particle(),
particle_slab.as_entire_binding_indirect(),
)),
);
BufferBindGroups { render }
});
}
gpu_buffer_operation_queue.create_bind_groups(&render_device, utils_pipeline);
for effect_batch in sorted_effect_batched.iter() {
#[cfg(feature = "trace")]
let _span_buffer = bevy::log::info_span!("create_batch_bind_groups").entered();
if let Some(property_key) = &effect_batch.property_key {
if let Err(err) = property_bind_groups.ensure_exists(
property_key,
&property_cache,
&spawner_buffer,
&prefix_sum_buffer,
&batch_info_buffer,
&render_device,
&pipeline_cache,
) {
error!("Failed to create property bind group for effect batch: {err:?}");
continue;
}
} else if let Err(err) = property_bind_groups.ensure_exists_no_property(
&property_cache,
&spawner_buffer,
&prefix_sum_buffer,
&batch_info_buffer,
&render_device,
&pipeline_cache,
) {
error!("Failed to create property bind group for effect batch: {err:?}");
continue;
}
if effect_cache
.create_particle_sim_bind_group(
&effect_batch.slab_id,
&render_device,
effect_batch.particle_layout.min_binding_size32(),
effect_batch.parent_min_binding_size,
effect_batch.parent_binding_source.as_ref(),
&pipeline_cache,
)
.is_err()
{
error!("No particle buffer allocated for effect batch.");
continue;
}
{
let consume_gpu_spawn_events = effect_batch
.layout_flags
.contains(LayoutFlags::CONSUME_GPU_SPAWN_EVENTS);
let consume_event_buffers = if let BatchSpawnInfo::GpuSpawner { .. } =
effect_batch.spawn_info
{
assert!(consume_gpu_spawn_events);
let cached_effect_events = effect_batch.cached_effect_events.as_ref().unwrap();
Some(ConsumeEventBuffers {
child_infos_buffer: event_cache.child_infos_buffer().unwrap(),
events: BufferSlice {
buffer: event_cache
.get_buffer(cached_effect_events.buffer_index)
.unwrap(),
offset: cached_effect_events.range.start * 4,
size: NonZeroU32::new(cached_effect_events.range.len() as u32 * 4).unwrap(),
},
})
} else {
assert!(!consume_gpu_spawn_events);
None
};
let Some(init_metadata_layout_desc) =
effect_cache.metadata_init_bind_group_layout_desc(consume_gpu_spawn_events)
else {
continue;
};
if effect_bind_groups
.get_or_create_init_metadata(
effect_batch,
&render_device,
&pipeline_cache.get_bind_group_layout(init_metadata_layout_desc),
effects_meta.effect_metadata_buffer.buffer().unwrap(),
consume_event_buffers,
)
.is_err()
{
continue;
}
}
{
let num_event_buffers = effect_batch.child_event_buffers.len() as u32;
let Some(update_metadata_layout_desc) =
effect_cache.metadata_update_bind_group_layout_desc(num_event_buffers)
else {
continue;
};
if effect_bind_groups
.get_or_create_update_metadata(
effect_batch,
&render_device,
&pipeline_cache.get_bind_group_layout(update_metadata_layout_desc),
effects_meta.effect_metadata_buffer.buffer().unwrap(),
event_cache.child_infos_buffer(),
&effect_batch.child_event_buffers[..],
)
.is_err()
{
continue;
}
}
if effect_batch.layout_flags.contains(LayoutFlags::RIBBONS) {
let effect_buffer = effect_cache.get_slab(&effect_batch.slab_id).unwrap();
let particle_buffer = effect_buffer.particle_buffer();
let indirect_index_buffer = effect_buffer.indirect_index_buffer();
let effect_metadata_buffer = effects_meta.effect_metadata_buffer.buffer().unwrap();
if let Err(err) = sort_bind_groups.ensure_sort_fill_bind_group(
&effect_batch.particle_layout,
particle_buffer,
indirect_index_buffer,
effect_metadata_buffer,
&spawner_buffer,
&pipeline_cache,
) {
error!(
"Failed to create sort-fill bind group @0 for ribbon effect: {:?}",
err
);
continue;
}
if let Err(err) = sort_bind_groups.ensure_sort_bind_group(&pipeline_cache) {
error!(
"Failed to create sort bind group @0 for ribbon effect: {:?}",
err
);
continue;
}
let indirect_index_buffer = effect_buffer.indirect_index_buffer();
if let Err(err) = sort_bind_groups.ensure_sort_copy_bind_group(
indirect_index_buffer,
effect_metadata_buffer,
&spawner_buffer,
&pipeline_cache,
) {
error!(
"Failed to create sort-copy bind group @0 for ribbon effect: {:?}",
err
);
continue;
}
}
if !effect_batch.texture_layout.layout.is_empty() {
let Some(material_bind_group_layout_desc) =
render_pipeline.get_material(&effect_batch.texture_layout)
else {
error!(
"Failed to find material bind group layout for particle slab #{}",
effect_batch.slab_id.index()
);
continue;
};
let material = Material {
layout: effect_batch.texture_layout.clone(),
textures: effect_batch.textures.iter().map(|h| h.id()).collect(),
};
assert_eq!(material.layout.layout.len(), material.textures.len());
let Ok(bind_group_entries) = material.make_entries(&gpu_images) else {
trace!(
"Temporarily ignoring material {:?} due to missing image(s)",
material
);
continue;
};
effect_bind_groups
.material_bind_groups
.entry(material.clone())
.or_insert_with(|| {
debug!("Creating material bind group for material {:?}", material);
render_device.create_bind_group(
&format!(
"hanabi:material_bind_group_{}",
material.layout.layout.len()
)[..],
&pipeline_cache.get_bind_group_layout(material_bind_group_layout_desc),
&bind_group_entries[..],
)
});
}
}
}
type DrawEffectsSystemState = SystemState<(
SRes<EffectsMeta>,
SRes<EffectBindGroups>,
SRes<PropertyBindGroups>,
SRes<PipelineCache>,
SRes<RenderAssets<RenderMesh>>,
SRes<MeshAllocator>,
SQuery<Read<ViewUniformOffset>>,
SRes<SortedEffectBatches>,
SQuery<Read<EffectDrawBatch>>,
)>;
pub(crate) struct DrawEffects {
params: DrawEffectsSystemState,
}
impl DrawEffects {
pub fn new(world: &mut World) -> Self {
Self {
params: SystemState::new(world),
}
}
}
fn draw<'w>(
world: &'w World,
pass: &mut TrackedRenderPass<'w>,
view: Entity,
entity: (Entity, MainEntity),
pipeline_id: CachedRenderPipelineId,
params: &mut DrawEffectsSystemState,
) {
let (
effects_meta,
effect_bind_groups,
property_bind_groups,
pipeline_cache,
meshes,
mesh_allocator,
views,
sorted_effect_batches,
effect_draw_batches,
) = params.get(world).unwrap();
let view_uniform = views.get(view).unwrap();
let effects_meta = effects_meta.into_inner();
let effect_bind_groups = effect_bind_groups.into_inner();
let property_bind_groups = property_bind_groups.into_inner();
let meshes = meshes.into_inner();
let mesh_allocator = mesh_allocator.into_inner();
let effect_draw_batch = effect_draw_batches.get(entity.0).unwrap();
let effect_batch = sorted_effect_batches
.get(effect_draw_batch.effect_batch_index)
.unwrap();
let Some(pipeline) = pipeline_cache.into_inner().get_render_pipeline(pipeline_id) else {
return;
};
trace!("render pass");
pass.set_render_pipeline(pipeline);
let Some(render_mesh): Option<&RenderMesh> = meshes.get(effect_batch.mesh) else {
return;
};
let Some(vertex_buffer_slice) = mesh_allocator.mesh_vertex_slice(&effect_batch.mesh) else {
return;
};
pass.set_vertex_buffer(0, vertex_buffer_slice.buffer.slice(..));
pass.set_bind_group(
0,
effects_meta.view_bind_group.as_ref().unwrap(),
&[view_uniform.offset],
);
pass.set_bind_group(
1,
effect_bind_groups
.particle_render(&effect_batch.slab_id)
.unwrap(),
&[],
);
let batch_info_aligned_size = effects_meta.batch_info_buffer.aligned_size();
let batch_info_offset = effect_batch.batch_info_id * batch_info_aligned_size as u32;
pass.set_bind_group(
2,
property_bind_groups
.get(effect_batch.property_key.as_ref())
.unwrap(),
&[batch_info_offset],
);
let material = Material {
layout: effect_batch.texture_layout.clone(),
textures: effect_batch.textures.iter().map(|h| h.id()).collect(),
};
if !effect_batch.texture_layout.layout.is_empty() {
if let Some(bind_group) = effect_bind_groups.material_bind_groups.get(&material) {
pass.set_bind_group(3, bind_group, &[]);
} else {
trace!(
"Particle material bind group not available for batch slab_id={}. Skipping draw call.",
effect_batch.slab_id.index(),
);
return;
}
}
let draw_indirect_index = effect_batch.draw_indirect_buffer_row_index.0;
assert_eq!(GpuDrawIndexedIndirectArgs::SHADER_SIZE.get(), 20);
let draw_indirect_offset =
draw_indirect_index as u64 * GpuDrawIndexedIndirectArgs::SHADER_SIZE.get();
trace!(
"Draw up to {} particles with {} vertices per particle for batch from particle slab #{} \
(effect_metadata_index={}, draw_indirect_offset={}B).",
effect_batch.slice.len(),
render_mesh.vertex_count,
effect_batch.slab_id.index(),
draw_indirect_index,
draw_indirect_offset,
);
let Some(indirect_buffer) = effects_meta.draw_indirect_buffer.buffer() else {
trace!(
"The draw indirect buffer containing the indirect draw args is not ready for batch slab_id=#{}. Skipping draw call.",
effect_batch.slab_id.index(),
);
return;
};
match render_mesh.buffer_info {
RenderMeshBufferInfo::Indexed { index_format, .. } => {
let Some(index_buffer_slice) = mesh_allocator.mesh_index_slice(&effect_batch.mesh)
else {
trace!(
"The index buffer for indexed rendering is not ready for batch slab_id=#{}. Skipping draw call.",
effect_batch.slab_id.index(),
);
return;
};
pass.set_index_buffer(index_buffer_slice.buffer.slice(..), index_format);
pass.draw_indexed_indirect(indirect_buffer, draw_indirect_offset);
}
RenderMeshBufferInfo::NonIndexed => {
pass.draw_indirect(indirect_buffer, draw_indirect_offset);
}
}
}
#[cfg(feature = "2d")]
impl Draw<Transparent2d> for DrawEffects {
fn draw<'w>(
&mut self,
world: &'w World,
pass: &mut TrackedRenderPass<'w>,
view: Entity,
item: &Transparent2d,
) -> Result<(), DrawError> {
trace!("Draw<Transparent2d>: view={:?}", view);
draw(
world,
pass,
view,
item.entity,
item.pipeline,
&mut self.params,
);
Ok(())
}
}
#[cfg(feature = "3d")]
impl Draw<Transparent3d> for DrawEffects {
fn draw<'w>(
&mut self,
world: &'w World,
pass: &mut TrackedRenderPass<'w>,
view: Entity,
item: &Transparent3d,
) -> Result<(), DrawError> {
trace!("Draw<Transparent3d>: view={:?}", view);
draw(
world,
pass,
view,
item.entity,
item.pipeline,
&mut self.params,
);
Ok(())
}
}
#[cfg(feature = "3d")]
impl Draw<AlphaMask3d> for DrawEffects {
fn draw<'w>(
&mut self,
world: &'w World,
pass: &mut TrackedRenderPass<'w>,
view: Entity,
item: &AlphaMask3d,
) -> Result<(), DrawError> {
trace!("Draw<AlphaMask3d>: view={:?}", view);
draw(
world,
pass,
view,
item.representative_entity,
item.batch_set_key.pipeline,
&mut self.params,
);
Ok(())
}
}
#[cfg(feature = "3d")]
impl Draw<Opaque3d> for DrawEffects {
fn draw<'w>(
&mut self,
world: &'w World,
pass: &mut TrackedRenderPass<'w>,
view: Entity,
item: &Opaque3d,
) -> Result<(), DrawError> {
trace!("Draw<Opaque3d>: view={:?}", view);
draw(
world,
pass,
view,
item.representative_entity,
item.batch_set_key.pipeline,
&mut self.params,
);
Ok(())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
enum HanabiPipelineId {
Invalid,
Cached(CachedComputePipelineId),
}
#[derive(Debug)]
pub(crate) enum ComputePipelineError {
Queued,
Creating,
Error,
}
impl From<&CachedPipelineState> for ComputePipelineError {
fn from(value: &CachedPipelineState) -> Self {
match value {
CachedPipelineState::Queued => Self::Queued,
CachedPipelineState::Creating(_) => Self::Creating,
CachedPipelineState::Err(_) => Self::Error,
_ => panic!("Trying to convert Ok state to error."),
}
}
}
pub(crate) struct HanabiComputePass<'a> {
pipeline_cache: &'a PipelineCache,
compute_pass: ComputePass<'a>,
pipeline_id: HanabiPipelineId,
}
impl<'a> Deref for HanabiComputePass<'a> {
type Target = ComputePass<'a>;
fn deref(&self) -> &Self::Target {
&self.compute_pass
}
}
impl DerefMut for HanabiComputePass<'_> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.compute_pass
}
}
impl<'a> HanabiComputePass<'a> {
pub fn new(pipeline_cache: &'a PipelineCache, compute_pass: ComputePass<'a>) -> Self {
Self {
pipeline_cache,
compute_pass,
pipeline_id: HanabiPipelineId::Invalid,
}
}
pub fn set_cached_compute_pipeline(
&mut self,
pipeline_id: CachedComputePipelineId,
) -> Result<(), ComputePipelineError> {
if HanabiPipelineId::Cached(pipeline_id) == self.pipeline_id {
trace!("set_cached_compute_pipeline() id={pipeline_id:?} -> already set; skipped");
return Ok(());
}
trace!("set_cached_compute_pipeline() id={pipeline_id:?}");
let Some(pipeline) = self.pipeline_cache.get_compute_pipeline(pipeline_id) else {
let state = self.pipeline_cache.get_compute_pipeline_state(pipeline_id);
if let CachedPipelineState::Err(err) = state {
error!(
"Failed to find compute pipeline #{}: {:?}",
pipeline_id.id(),
err
);
} else {
debug!("Compute pipeline not ready #{}", pipeline_id.id());
}
return Err(state.into());
};
self.compute_pass.set_pipeline(pipeline);
self.pipeline_id = HanabiPipelineId::Cached(pipeline_id);
Ok(())
}
}
fn begin_hanabi_compute_pass<'encoder>(
label: &str,
pipeline_cache: &'encoder PipelineCache,
render_context: &'encoder mut RenderContext,
) -> HanabiComputePass<'encoder> {
let compute_pass =
render_context
.command_encoder()
.begin_compute_pass(&ComputePassDescriptor {
label: Some(label),
timestamp_writes: None,
});
HanabiComputePass::new(pipeline_cache, compute_pass)
}
fn simulate(
mut render_context: RenderContext,
pipeline_cache: Res<PipelineCache>,
effects_meta: Res<EffectsMeta>,
effect_bind_groups: Res<EffectBindGroups>,
property_bind_groups: Res<PropertyBindGroups>,
sort_bind_groups: Res<SortBindGroups>,
utils_pipeline: Res<UtilsPipeline>,
effect_cache: Res<EffectCache>,
event_cache: Res<EventCache>,
gpu_buffer_operations: Res<GpuBufferOperations>,
sorted_effect_batches: Res<SortedEffectBatches>,
init_fill_dispatch_queue: Res<InitFillDispatchQueue>,
) {
trace!("simulate()");
{
let command_encoder = render_context.command_encoder();
effects_meta
.dispatch_indirect_buffer
.write_buffers(command_encoder);
effects_meta
.draw_indirect_buffer
.write_buffer(command_encoder);
effects_meta
.effect_metadata_buffer
.write_buffer(command_encoder);
event_cache.write_buffers(command_encoder);
sort_bind_groups.write_buffers(command_encoder);
}
if let Some(queue_index) = init_fill_dispatch_queue.submitted_queue_index.as_ref() {
gpu_buffer_operations.dispatch(
*queue_index,
&mut render_context,
&utils_pipeline,
Some("hanabi:init_indirect_fill_dispatch"),
);
}
if sorted_effect_batches.is_empty() {
return;
}
let (
Some(_),
true,
Some(indirect_metadata_bind_group),
Some(init_and_indirect_sim_params_bind_group),
Some(indirect_spawner_bind_group),
) = (
effects_meta.spawner_buffer.buffer(),
!effects_meta.spawner_buffer.is_empty(),
&effects_meta.indirect_metadata_bind_group,
&effects_meta.init_and_indirect_sim_params_bind_group,
&effects_meta.indirect_spawner_bind_group,
)
else {
trace!("WARN - Some resource not ready, can't simulate this frame.");
return;
};
let Some(prefix_sum_bind_group) = &effects_meta.prefix_sum_bind_group else {
trace!("WARN - Prefix sum bind group not ready, can't simulate this frame.");
return;
};
let Some(indirect_buffer) = effects_meta.dispatch_indirect_buffer.buffer() else {
trace!("WARN - Missing indirect buffer for update pass, can't simulate this frame.");
return;
};
{
trace!("init: loop over effect batches...");
let mut compute_pass =
begin_hanabi_compute_pass("hanabi:init", &pipeline_cache, &mut render_context);
compute_pass.set_bind_group(
0,
effects_meta
.init_and_indirect_sim_params_bind_group
.as_ref()
.unwrap(),
&[],
);
for effect_batch in sorted_effect_batches.iter() {
{
let use_indirect_dispatch = effect_batch
.layout_flags
.contains(LayoutFlags::CONSUME_GPU_SPAWN_EVENTS);
match effect_batch.spawn_info {
BatchSpawnInfo::CpuSpawner { total_spawn_count } => {
assert!(!use_indirect_dispatch);
if total_spawn_count == 0 {
continue;
}
}
BatchSpawnInfo::GpuSpawner { .. } => {
assert!(use_indirect_dispatch);
}
}
}
let Some(particle_bind_group) =
effect_cache.particle_sim_bind_group(&effect_batch.slab_id)
else {
error!(
"Failed to find init particle@1 bind group for slab #{}",
effect_batch.slab_id.index()
);
continue;
};
let Some(metadata_bind_group) = effect_bind_groups
.init_metadata_bind_groups
.get(&effect_batch.slab_id)
else {
error!(
"Failed to find init metadata@3 bind group for slab #{}",
effect_batch.slab_id.index()
);
continue;
};
if compute_pass
.set_cached_compute_pipeline(effect_batch.init_and_update_pipeline_ids.init)
.is_err()
{
error!("Failed to set init pipeline");
continue;
}
let spawner_base = effect_batch.spawner_base;
let spawner_aligned_size = effects_meta.spawner_buffer.aligned_size();
debug_assert!(spawner_aligned_size >= GpuSpawnerParams::min_size().get() as usize);
let spawner_offset = spawner_base * spawner_aligned_size as u32;
let batch_info_aligned_size = effects_meta.batch_info_buffer.aligned_size();
let batch_info_offset = effect_batch.batch_info_id * batch_info_aligned_size as u32;
compute_pass.set_bind_group(1, particle_bind_group, &[]);
compute_pass.set_bind_group(
2,
property_bind_groups
.get(effect_batch.property_key.as_ref())
.unwrap(),
&[batch_info_offset],
);
compute_pass.set_bind_group(3, &metadata_bind_group.bind_group, &[]);
match effect_batch.spawn_info {
BatchSpawnInfo::GpuSpawner {
init_indirect_dispatch_index,
..
} => {
assert!(effect_batch
.layout_flags
.contains(LayoutFlags::CONSUME_GPU_SPAWN_EVENTS));
assert_eq!(GpuDispatchIndirectArgs::min_size().get(), 12);
let indirect_offset = init_indirect_dispatch_index as u64 * 12;
trace!(
"record commands for indirect init pipeline of effect {:?} \
init_indirect_dispatch_index={} \
indirect_offset={} \
spawner_base={} \
spawner_offset={} \
property_key={:?}...",
effect_batch.handle,
init_indirect_dispatch_index,
indirect_offset,
spawner_base,
spawner_offset,
effect_batch.property_key,
);
compute_pass.dispatch_workgroups_indirect(
event_cache.init_indirect_dispatch_buffer().unwrap(),
indirect_offset,
);
}
BatchSpawnInfo::CpuSpawner {
total_spawn_count: spawn_count,
} => {
assert!(!effect_batch
.layout_flags
.contains(LayoutFlags::CONSUME_GPU_SPAWN_EVENTS));
const WORKGROUP_SIZE: u32 = 64;
let workgroup_count = spawn_count.div_ceil(WORKGROUP_SIZE);
trace!(
"record commands for init pipeline of effect {:?} \
(spawn {} particles => {} workgroups) spawner_base={} \
spawner_offset={} \
property_key={:?}...",
effect_batch.handle,
spawn_count,
workgroup_count,
spawner_base,
spawner_offset,
effect_batch.property_key,
);
compute_pass.dispatch_workgroups(workgroup_count, 1, 1);
}
}
trace!("init compute dispatched");
}
}
{
let mut compute_pass = begin_hanabi_compute_pass(
"hanabi:indirect_dispatch",
&pipeline_cache,
&mut render_context,
);
trace!("record commands for indirect dispatch pipeline...");
let has_gpu_spawn_events = !event_cache.child_infos().is_empty();
if has_gpu_spawn_events {
if let Some(indirect_child_info_buffer_bind_group) =
event_cache.indirect_child_info_buffer_bind_group()
{
assert!(has_gpu_spawn_events);
compute_pass.set_bind_group(3, indirect_child_info_buffer_bind_group, &[]);
} else {
error!("Missing child_info_buffer@3 bind group for the vfx_indirect pass.");
compute_pass.insert_debug_marker("ERROR:MissingIndirectBindGroup3");
return;
}
}
if compute_pass
.set_cached_compute_pipeline(effects_meta.active_indirect_pipeline_id)
.is_err()
{
compute_pass.insert_debug_marker("ERROR:FailedToSetIndirectComputePipeline");
return;
}
const WORKGROUP_SIZE: u32 = 64;
let total_effect_count = effects_meta.spawner_buffer.len() as u32;
let workgroup_count = total_effect_count.div_ceil(WORKGROUP_SIZE);
compute_pass.set_bind_group(0, init_and_indirect_sim_params_bind_group, &[]);
compute_pass.set_bind_group(1, indirect_metadata_bind_group, &[]);
compute_pass.set_bind_group(2, indirect_spawner_bind_group, &[]);
compute_pass.dispatch_workgroups(workgroup_count, 1, 1);
trace!(
"indirect dispatch compute dispatched: total_effect_count={} workgroup_count={}",
total_effect_count,
workgroup_count
);
}
{
let mut compute_pass = begin_hanabi_compute_pass(
"hanabi:update_prefix_sum",
&pipeline_cache,
&mut render_context,
);
trace!("record commands for update prefix sum pipeline...");
if compute_pass
.set_cached_compute_pipeline(effects_meta.prefix_sum_pipeline_id)
.is_err()
{
trace!("ERROR - Failed to set prefix sum compute pipeline. Simulation aborted.");
return;
}
const WORKGROUP_SIZE: u32 = 64;
let total_batch_count = effects_meta.batch_info_buffer.len() as u32;
let workgroup_count = total_batch_count.div_ceil(WORKGROUP_SIZE);
compute_pass.set_bind_group(0, prefix_sum_bind_group, &[]);
compute_pass.dispatch_workgroups(workgroup_count, 1, 1);
trace!(
"prefix sum dispatched: total_batch_count={} workgroup_count={}",
total_batch_count,
workgroup_count
);
}
let mut needs_sort = false;
{
let mut compute_pass =
begin_hanabi_compute_pass("hanabi:update", &pipeline_cache, &mut render_context);
compute_pass.set_bind_group(
0,
effects_meta.update_sim_params_bind_group.as_ref().unwrap(),
&[],
);
for effect_batch in sorted_effect_batches.iter() {
if effect_batch.layout_flags.contains(LayoutFlags::RIBBONS) {
needs_sort = true;
}
let Some(particle_bind_group) =
effect_cache.particle_sim_bind_group(&effect_batch.slab_id)
else {
error!(
"Failed to find update particle@1 bind group for slab #{}",
effect_batch.slab_id.index()
);
compute_pass.insert_debug_marker("ERROR:MissingParticleSimBindGroup");
continue;
};
let Some(metadata_bind_group) = effect_bind_groups
.update_metadata_bind_groups
.get(&effect_batch.slab_id)
else {
error!(
"Failed to find update metadata@3 bind group for slab #{}",
effect_batch.slab_id.index()
);
compute_pass.insert_debug_marker("ERROR:MissingMetadataBindGroup");
continue;
};
if let Err(err) = compute_pass
.set_cached_compute_pipeline(effect_batch.init_and_update_pipeline_ids.update)
{
compute_pass.insert_debug_marker(&format!(
"ERROR:FailedToSetCachedUpdatePipeline:{:?}",
err
));
continue;
}
let batch_info_aligned_size = effects_meta.batch_info_buffer.aligned_size();
let batch_info_offset = effect_batch.batch_info_id * batch_info_aligned_size as u32;
trace!(
"record commands for update pipeline of effect {:?} spawner_base={} batch_info_id={} batch_info_offset={}B",
effect_batch.handle,
effect_batch.spawner_base,
effect_batch.batch_info_id,
batch_info_offset,
);
compute_pass.set_bind_group(1, particle_bind_group, &[]);
compute_pass.set_bind_group(
2,
property_bind_groups
.get(effect_batch.property_key.as_ref())
.unwrap(),
&[batch_info_offset],
);
compute_pass.set_bind_group(3, &metadata_bind_group.bind_group, &[]);
let dispatch_indirect_offset =
effect_batch.batch_info_id * GpuDispatchIndirectArgs::SHADER_SIZE.get() as u32;
trace!(
"dispatch_workgroups_indirect: buffer={:?} offset={}B",
indirect_buffer,
dispatch_indirect_offset,
);
compute_pass
.dispatch_workgroups_indirect(indirect_buffer, dispatch_indirect_offset as u64);
trace!("update compute dispatched");
}
}
if needs_sort {
{
let mut compute_pass = begin_hanabi_compute_pass(
"hanabi:sort_prefix_sum",
&pipeline_cache,
&mut render_context,
);
trace!("record commands for sort prefix sum pass...");
if compute_pass
.set_cached_compute_pipeline(effects_meta.prefix_sum_pipeline_id)
.is_err()
{
trace!("ERROR - Failed to set prefix sum compute pipeline. Simulation aborted.");
return;
}
const WORKGROUP_SIZE: u32 = 64;
let total_batch_count = effects_meta.batch_info_buffer.len() as u32;
let workgroup_count = total_batch_count.div_ceil(WORKGROUP_SIZE);
compute_pass.set_bind_group(0, prefix_sum_bind_group, &[]);
compute_pass.dispatch_workgroups(workgroup_count, 1, 1);
trace!(
"prefix sum dispatched: total_batch_count={} workgroup_count={}",
total_batch_count,
workgroup_count
);
}
if let Some(queue_index) = sorted_effect_batches.dispatch_queue_index.as_ref() {
gpu_buffer_operations.dispatch(
*queue_index,
&mut render_context,
&utils_pipeline,
Some("hanabi:sort_fill_dispatch"),
);
}
{
let mut compute_pass =
begin_hanabi_compute_pass("hanabi:sort", &pipeline_cache, &mut render_context);
let effect_metadata_buffer = effects_meta.effect_metadata_buffer.buffer().unwrap();
let indirect_buffer = sort_bind_groups.indirect_buffer().unwrap();
for effect_batch in sorted_effect_batches.iter() {
trace!("Processing effect batch for sorting...");
if !effect_batch.layout_flags.contains(LayoutFlags::RIBBONS) {
continue;
}
assert!(effect_batch.particle_layout.contains(Attribute::RIBBON_ID));
assert!(effect_batch.particle_layout.contains(Attribute::AGE));
let Some(effect_buffer) = effect_cache.get_slab(&effect_batch.slab_id) else {
warn!("Missing sort-fill effect buffer.");
continue;
};
let indirect_dispatch_index = *effect_batch
.sort_fill_indirect_dispatch_index
.as_ref()
.unwrap();
let indirect_offset =
sort_bind_groups.get_indirect_dispatch_byte_offset(indirect_dispatch_index);
{
compute_pass.push_debug_group("hanabi:sort_fill");
let Some(pipeline_id) =
sort_bind_groups.get_sort_fill_pipeline_id(&effect_batch.particle_layout)
else {
warn!("Missing sort-fill pipeline.");
compute_pass.insert_debug_marker("ERROR:MissingSortFillPipeline");
continue;
};
if compute_pass
.set_cached_compute_pipeline(pipeline_id)
.is_err()
{
compute_pass.insert_debug_marker("ERROR:FailedToSetSortFillPipeline");
compute_pass.pop_debug_group();
return;
}
let spawner_base = effect_batch.spawner_base;
let spawner_aligned_size = effects_meta.spawner_buffer.aligned_size();
assert!(spawner_aligned_size >= GpuSpawnerParams::min_size().get() as usize);
let spawner_offset = spawner_base * spawner_aligned_size as u32;
let particle_buffer = effect_buffer.particle_buffer();
let indirect_index_buffer = effect_buffer.indirect_index_buffer();
let Some(spawner_buffer) = effects_meta.spawner_buffer.buffer() else {
warn!("Missing spawner buffer for sort-fill.");
compute_pass.insert_debug_marker("ERROR:MissingSortFillSpawnerBuffer");
continue;
};
let Some(bind_group) = sort_bind_groups.sort_fill_bind_group(
particle_buffer.id(),
indirect_index_buffer.id(),
effect_metadata_buffer.id(),
spawner_buffer.id(),
) else {
warn!("Missing sort-fill bind group.");
compute_pass.insert_debug_marker("ERROR:MissingSortFillBindGroup");
continue;
};
let effect_metadata_offset = effects_meta
.gpu_limits
.effect_metadata_offset(effect_batch.metadata_table_id.0)
as u32;
compute_pass.set_bind_group(
0,
bind_group,
&[effect_metadata_offset, spawner_offset],
);
compute_pass
.dispatch_workgroups_indirect(indirect_buffer, indirect_offset as u64);
trace!("Dispatched sort-fill with indirect offset +{indirect_offset}");
compute_pass.pop_debug_group();
}
{
compute_pass.push_debug_group("hanabi:sort");
if compute_pass
.set_cached_compute_pipeline(sort_bind_groups.sort_pipeline_id())
.is_err()
{
compute_pass.insert_debug_marker("ERROR:FailedToSetSortPipeline");
compute_pass.pop_debug_group();
return;
}
let Some(bind_group) = sort_bind_groups.sort_bind_group() else {
warn!("Missing sort bind group.");
compute_pass.insert_debug_marker("ERROR:MissingSortBindGroup");
continue;
};
compute_pass.set_bind_group(0, bind_group, &[]);
compute_pass
.dispatch_workgroups_indirect(indirect_buffer, indirect_offset as u64);
trace!("Dispatched sort with indirect offset +{indirect_offset}");
compute_pass.pop_debug_group();
}
{
compute_pass.push_debug_group("hanabi:copy_sorted_indices");
let pipeline_id = sort_bind_groups.get_sort_copy_pipeline_id();
if compute_pass
.set_cached_compute_pipeline(pipeline_id)
.is_err()
{
compute_pass.insert_debug_marker("ERROR:FailedToSetSortCopyPipeline");
compute_pass.pop_debug_group();
return;
}
let spawner_base = effect_batch.spawner_base;
let spawner_aligned_size = effects_meta.spawner_buffer.aligned_size();
assert!(spawner_aligned_size >= GpuSpawnerParams::min_size().get() as usize);
let spawner_offset = spawner_base * spawner_aligned_size as u32;
let indirect_index_buffer = effect_buffer.indirect_index_buffer();
let Some(spawner_buffer) = effects_meta.spawner_buffer.buffer() else {
warn!("Missing spawner buffer for sort-copy.");
compute_pass.insert_debug_marker("ERROR:MissingSortCopySpawnerBuffer");
continue;
};
let Some(bind_group) = sort_bind_groups.sort_copy_bind_group(
indirect_index_buffer.id(),
effect_metadata_buffer.id(),
spawner_buffer.id(),
) else {
warn!("Missing sort-copy bind group.");
compute_pass.insert_debug_marker("ERROR:MissingSortCopyBindGroup");
continue;
};
let effect_metadata_offset = effects_meta
.effect_metadata_buffer
.dynamic_offset(effect_batch.metadata_table_id);
compute_pass.set_bind_group(
0,
bind_group,
&[effect_metadata_offset, spawner_offset],
);
compute_pass
.dispatch_workgroups_indirect(indirect_buffer, indirect_offset as u64);
trace!("Dispatched sort-copy with indirect offset +{indirect_offset}");
compute_pass.pop_debug_group();
}
}
}
}
}
impl From<LayoutFlags> for ParticleRenderAlphaMaskPipelineKey {
fn from(layout_flags: LayoutFlags) -> Self {
if layout_flags.contains(LayoutFlags::USE_ALPHA_MASK) {
ParticleRenderAlphaMaskPipelineKey::AlphaMask
} else if layout_flags.contains(LayoutFlags::OPAQUE) {
ParticleRenderAlphaMaskPipelineKey::Opaque
} else {
ParticleRenderAlphaMaskPipelineKey::Blend
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn layout_flags() {
let flags = LayoutFlags::default();
assert_eq!(flags, LayoutFlags::NONE);
}
#[cfg(feature = "gpu_tests")]
#[test]
fn gpu_limits() {
use crate::test_utils::MockRenderer;
let renderer = MockRenderer::new();
let device = renderer.device();
let limits = GpuLimits::from_device(&device);
assert!(limits.effect_metadata_offset(256) >= 256 * GpuEffectMetadata::min_size().get());
}
#[cfg(feature = "gpu_tests")]
#[test]
fn gpu_ops_ifda() {
use crate::test_utils::MockRenderer;
let renderer = MockRenderer::new();
let device = renderer.device();
let render_queue = renderer.queue();
let mut world = World::new();
world.insert_resource(device.clone());
let mut buffer_ops = GpuBufferOperations::from_world(&mut world);
let src_buffer = device.create_buffer(&BufferDescriptor {
label: None,
size: 256,
usage: BufferUsages::STORAGE,
mapped_at_creation: false,
});
let dst_buffer = device.create_buffer(&BufferDescriptor {
label: None,
size: 256,
usage: BufferUsages::STORAGE,
mapped_at_creation: false,
});
buffer_ops.begin_frame();
{
let mut q = InitFillDispatchQueue::default();
q.enqueue(0, 0);
assert_eq!(q.queue.len(), 1);
q.enqueue(1, 1);
assert_eq!(q.queue.len(), 2);
q.submit(&src_buffer, &dst_buffer, &mut buffer_ops);
assert_eq!(buffer_ops.args_buffer.len(), 1);
}
buffer_ops.end_frame(&device, &render_queue);
buffer_ops.begin_frame();
{
let mut q = InitFillDispatchQueue::default();
q.enqueue(1, 1);
assert_eq!(q.queue.len(), 1);
q.enqueue(0, 0);
assert_eq!(q.queue.len(), 2);
q.submit(&src_buffer, &dst_buffer, &mut buffer_ops);
assert_eq!(buffer_ops.args_buffer.len(), 1);
}
buffer_ops.end_frame(&device, &render_queue);
buffer_ops.begin_frame();
{
let mut q = InitFillDispatchQueue::default();
q.enqueue(0, 1);
assert_eq!(q.queue.len(), 1);
q.enqueue(1, 0);
assert_eq!(q.queue.len(), 2);
q.submit(&src_buffer, &dst_buffer, &mut buffer_ops);
assert_eq!(buffer_ops.args_buffer.len(), 2);
}
buffer_ops.end_frame(&device, &render_queue);
}
}