use ash::vk;
use concinnity_core::gfx::frustum::Frustum;
use concinnity_core::gfx::render_types::ParticleParams;
use concinnity_core::render::error::{RenderError, RenderResult};
use concinnity_core::render::particles::{
ParticleEmitterRecord, ParticleSpawnState, ParticleSpawns, spawn_seed,
};
use concinnity_core::render::reactive_mask::ReactiveWrite;
use concinnity_core::render::uniforms::GpuParticle;
use concinnity_core::render::uniforms::ParticleView;
use std::cell::RefCell;
use super::allocator::PooledBuffer;
use super::context::{HDR_FORMAT, VkContext};
use super::descriptor_layout::{Binding, PoolSizes};
use super::pipeline_desc::{GraphicsPipelineDesc, compute_pipeline};
use super::reactive_mask::{self, WriterRenderPasses};
use super::record::cmd_push_constants;
use super::resources::{alloc_descriptor_sets, create_descriptor_set_layout};
use super::set_writes::SetWrites;
use super::texture::GpuUploadContext;
use crate::vulkan::builtin_shaders::CompileProgram;
use crate::vulkan::owned::{
OwnedDescriptorPool, OwnedFramebuffer, OwnedPipeline, OwnedPipelineLayout, OwnedRenderPass,
OwnedSampler, OwnedSetLayout, VkDevice,
};
pub(in crate::vulkan) const MAX_EMITTERS: usize = 256;
type ParticleShaderSpirv = (Vec<u8>, Vec<u8>, Vec<u8>);
pub(in crate::vulkan) fn compile_particle_shaders(
hot_reload: bool,
msaa: bool,
) -> RenderResult<ParticleShaderSpirv> {
let cs = super::builtin_shaders::PARTICLE_SIMULATE.compile(hot_reload)?;
let vs = super::builtin_shaders::PARTICLE_VERT.compile(hot_reload)?;
let fs = super::builtin_shaders::PARTICLE_FRAG
.at(msaa)
.compile(hot_reload)?;
Ok((cs, vs, fs))
}
pub(in crate::vulkan) struct ParticleEmitterGpuState {
pub pool_buffer: PooledBuffer,
pub spawn_state: RefCell<ParticleSpawnState>,
pub compute_set: vk::DescriptorSet,
pub render_set: vk::DescriptorSet,
pub texture_slot: usize,
}
pub(in crate::vulkan) struct ParticleFrame {
pub dt: f32,
pub frame_index: u32,
pub spawns: Vec<ParticleSpawns>,
}
pub(in crate::vulkan) struct ParticleResources {
pub(in crate::vulkan) compute_pipeline: OwnedPipeline,
pub(in crate::vulkan) compute_pipeline_layout: OwnedPipelineLayout,
pub(in crate::vulkan) compute_set_layout: OwnedSetLayout,
pub(in crate::vulkan) render_passes: WriterRenderPasses,
pub(in crate::vulkan) render_pipeline: OwnedPipeline,
pub(in crate::vulkan) render_pipeline_layout: OwnedPipelineLayout,
pub(in crate::vulkan) _view_set_layout: OwnedSetLayout,
pub(in crate::vulkan) emitter_set_layout: OwnedSetLayout,
pub(in crate::vulkan) descriptor_pool: OwnedDescriptorPool,
pub(in crate::vulkan) view_ubos: Vec<PooledBuffer>,
pub(in crate::vulkan) view_sets: Vec<vk::DescriptorSet>,
pub(in crate::vulkan) framebuffers: Vec<OwnedFramebuffer>,
pub(in crate::vulkan) sampler: OwnedSampler,
msaa: bool,
}
#[derive(Clone, Copy)]
pub(in crate::vulkan) struct ParticlePassTargets<'a> {
pub(in crate::vulkan) hdr_resolve_views: &'a [vk::ImageView],
pub(in crate::vulkan) reactive_mask_views: &'a [vk::ImageView],
pub(in crate::vulkan) depth_views: &'a [vk::ImageView],
pub(in crate::vulkan) extent: vk::Extent2D,
}
fn create_framebuffers(
device: &VkDevice,
render_pass: vk::RenderPass,
targets: &ParticlePassTargets<'_>,
frames: usize,
) -> RenderResult<Vec<OwnedFramebuffer>> {
targets
.hdr_resolve_views
.iter()
.zip(targets.reactive_mask_views)
.take(frames)
.map(|(&scene, &mask)| {
let attachments = device.writer_targets().views(scene, mask);
let fb_info = vk::FramebufferCreateInfo::default()
.render_pass(render_pass)
.attachments(&attachments)
.width(targets.extent.width.max(1))
.height(targets.extent.height.max(1))
.layers(1);
device
.create_framebuffer(&fb_info)
.map_err(|e| super::error::map_vk_result(e, "particle framebuffer"))
})
.collect()
}
impl ParticleResources {
pub(in crate::vulkan) fn new(
gpu: &GpuUploadContext,
frames: usize,
targets: ParticlePassTargets,
msaa: bool,
hot_reload: bool,
) -> RenderResult<Self> {
let &GpuUploadContext { alloc, device, .. } = gpu;
let depth_views = targets.depth_views;
let render_passes = WriterRenderPasses::new(device.writer_targets(), |mask| {
create_render_pass(device, HDR_FORMAT, mask)
})?;
let compute_set_layout = create_descriptor_set_layout(device, &compute_set_bindings())?;
let view_set_layout = create_descriptor_set_layout(device, &view_set_bindings())?;
let emitter_set_layout = create_descriptor_set_layout(device, &emitter_set_bindings())?;
let compute_pipeline_layout =
create_compute_pipeline_layout(device, compute_set_layout.handle())?;
let render_pipeline_layout = create_render_pipeline_layout(
device,
view_set_layout.handle(),
emitter_set_layout.handle(),
)?;
let (cs_spv, vs_spv, fs_spv) = compile_particle_shaders(hot_reload, msaa)?;
let compute_pipeline = compute_pipeline(
device,
compute_pipeline_layout.handle(),
&cs_spv,
"particle compute",
)?;
let render_pipeline = create_render_pipeline(
device,
render_passes.compatible(),
render_pipeline_layout.handle(),
&vs_spv,
&fs_spv,
)?;
let view_size = std::mem::size_of::<ParticleView>() as u64;
let mut view_ubos = Vec::with_capacity(frames);
for _ in 0..frames {
let buf = alloc.create_buffer(
view_size,
vk::BufferUsageFlags::UNIFORM_BUFFER,
vk::MemoryPropertyFlags::HOST_VISIBLE | vk::MemoryPropertyFlags::HOST_COHERENT,
)?;
view_ubos.push(buf);
}
let sampler = create_sampler(device)?;
let descriptor_pool = create_descriptor_pool(device, frames)?;
let view_layouts: Vec<_> = (0..frames).map(|_| view_set_layout.handle()).collect();
let view_sets = alloc_descriptor_sets(device, descriptor_pool.handle(), &view_layouts)?;
for (i, &set) in view_sets.iter().enumerate() {
SetWrites::new(set)
.uniform_buffer(0, view_ubos[i].buffer(), view_size)
.sampled_image(1, frame_view(depth_views, i))
.apply(device);
}
let framebuffers =
create_framebuffers(device, render_passes.compatible(), &targets, frames)?;
Ok(Self {
compute_pipeline,
compute_pipeline_layout,
compute_set_layout,
render_passes,
render_pipeline,
render_pipeline_layout,
_view_set_layout: view_set_layout,
emitter_set_layout,
descriptor_pool,
view_ubos,
view_sets,
framebuffers,
sampler,
msaa,
})
}
pub(in crate::vulkan) fn rebuild(
&mut self,
device: &VkDevice,
targets: ParticlePassTargets<'_>,
) -> RenderResult<()> {
for (i, &set) in self.view_sets.iter().enumerate() {
write_depth_binding(device, set, frame_view(targets.depth_views, i));
}
self.framebuffers.clear();
self.framebuffers = create_framebuffers(
device,
self.render_passes.compatible(),
&targets,
self.view_ubos.len(),
)?;
Ok(())
}
pub(in crate::vulkan) fn rebuild_pipelines(
&self,
device: &VkDevice,
hot_reload: bool,
) -> RenderResult<(OwnedPipeline, OwnedPipeline)> {
let (cs_spv, vs_spv, fs_spv) = compile_particle_shaders(hot_reload, self.msaa)?;
let cp = compute_pipeline(
device,
self.compute_pipeline_layout.handle(),
&cs_spv,
"particle compute",
)?;
let rp = create_render_pipeline(
device,
self.render_passes.compatible(),
self.render_pipeline_layout.handle(),
&vs_spv,
&fs_spv,
)?;
Ok((cp, rp))
}
pub(in crate::vulkan) fn swap_pipelines(
&mut self,
compute: OwnedPipeline,
render: OwnedPipeline,
) {
self.compute_pipeline = compute;
self.render_pipeline = render;
}
pub(in crate::vulkan) fn destroy(&mut self, _device: &VkDevice) {
self.framebuffers.clear();
self.view_ubos.clear();
}
}
pub(in crate::vulkan) fn build_emitter_gpu_state(
gpu: GpuUploadContext,
resources: &ParticleResources,
record: &ParticleEmitterRecord,
) -> RenderResult<ParticleEmitterGpuState> {
let GpuUploadContext { alloc, device, .. } = gpu;
let slots = record.max_particles as u64;
let pool_bytes = slots * std::mem::size_of::<GpuParticle>() as u64;
let pool_buffer = alloc.create_buffer(
pool_bytes,
vk::BufferUsageFlags::STORAGE_BUFFER | vk::BufferUsageFlags::TRANSFER_DST,
vk::MemoryPropertyFlags::DEVICE_LOCAL,
)?;
zero_device_buffer(gpu, pool_buffer.buffer(), pool_bytes)?;
let set_layouts = [
resources.compute_set_layout.handle(),
resources.emitter_set_layout.handle(),
];
let sets = alloc_descriptor_sets(device, resources.descriptor_pool.handle(), &set_layouts)?;
let compute_set = sets[0];
let render_set = sets[1];
SetWrites::new(compute_set)
.storage_buffer(0, pool_buffer.buffer(), pool_bytes)
.apply(device);
SetWrites::new(render_set)
.storage_buffer(0, pool_buffer.buffer(), pool_bytes)
.sampler(2, resources.sampler.handle())
.apply(device);
Ok(ParticleEmitterGpuState {
pool_buffer,
spawn_state: RefCell::new(ParticleSpawnState::default()),
compute_set,
render_set,
texture_slot: usize::MAX,
})
}
fn create_render_pass(
device: &VkDevice,
format: vk::Format,
mask: Option<ReactiveWrite>,
) -> RenderResult<OwnedRenderPass> {
let attachment = vk::AttachmentDescription::default()
.format(format)
.samples(vk::SampleCountFlags::TYPE_1)
.load_op(vk::AttachmentLoadOp::LOAD)
.store_op(vk::AttachmentStoreOp::STORE)
.stencil_load_op(vk::AttachmentLoadOp::DONT_CARE)
.stencil_store_op(vk::AttachmentStoreOp::DONT_CARE)
.initial_layout(vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL)
.final_layout(vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL);
let attachments = device.writer_targets().attachments(attachment, mask);
let color_refs: Vec<_> = (0..attachments.len() as u32)
.map(|i| {
vk::AttachmentReference::default()
.attachment(i)
.layout(vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL)
})
.collect();
let subpass = vk::SubpassDescription::default()
.pipeline_bind_point(vk::PipelineBindPoint::GRAPHICS)
.color_attachments(&color_refs);
let dep_in = vk::SubpassDependency::default()
.src_subpass(vk::SUBPASS_EXTERNAL)
.dst_subpass(0)
.src_stage_mask(
vk::PipelineStageFlags::COLOR_ATTACHMENT_OUTPUT
| vk::PipelineStageFlags::FRAGMENT_SHADER,
)
.src_access_mask(vk::AccessFlags::SHADER_READ | vk::AccessFlags::COLOR_ATTACHMENT_WRITE)
.dst_stage_mask(vk::PipelineStageFlags::COLOR_ATTACHMENT_OUTPUT)
.dst_access_mask(
vk::AccessFlags::COLOR_ATTACHMENT_WRITE | vk::AccessFlags::COLOR_ATTACHMENT_READ,
);
let dep_out = vk::SubpassDependency::default()
.src_subpass(0)
.dst_subpass(vk::SUBPASS_EXTERNAL)
.src_stage_mask(vk::PipelineStageFlags::COLOR_ATTACHMENT_OUTPUT)
.src_access_mask(vk::AccessFlags::COLOR_ATTACHMENT_WRITE)
.dst_stage_mask(vk::PipelineStageFlags::FRAGMENT_SHADER)
.dst_access_mask(vk::AccessFlags::SHADER_READ);
let deps = [dep_in, dep_out];
let info = vk::RenderPassCreateInfo::default()
.attachments(&attachments)
.subpasses(std::slice::from_ref(&subpass))
.dependencies(&deps);
device
.create_render_pass(&info)
.map_err(|e| super::error::map_vk_result(e, "particle render pass"))
}
fn compute_set_bindings() -> [Binding; 1] {
[(
0,
vk::DescriptorType::STORAGE_BUFFER,
vk::ShaderStageFlags::COMPUTE,
)]
}
fn view_set_bindings() -> [Binding; 2] {
use vk::DescriptorType as T;
[
(0, T::UNIFORM_BUFFER, vk::ShaderStageFlags::VERTEX),
(1, T::SAMPLED_IMAGE, vk::ShaderStageFlags::FRAGMENT),
]
}
fn emitter_set_bindings() -> [Binding; 3] {
use vk::DescriptorType as T;
let frag = vk::ShaderStageFlags::FRAGMENT;
[
(0, T::STORAGE_BUFFER, vk::ShaderStageFlags::VERTEX),
(1, T::SAMPLED_IMAGE, frag),
(2, T::SAMPLER, frag),
]
}
const PARTICLE_PUSH_BYTES: u32 = std::mem::size_of::<ParticleParams>() as u32;
const _: () = assert!(PARTICLE_PUSH_BYTES <= 128);
fn create_compute_pipeline_layout(
device: &VkDevice,
compute_set_layout: vk::DescriptorSetLayout,
) -> RenderResult<OwnedPipelineLayout> {
let push_range = vk::PushConstantRange::default()
.stage_flags(vk::ShaderStageFlags::COMPUTE)
.offset(0)
.size(PARTICLE_PUSH_BYTES);
let set_layouts = [compute_set_layout];
let info = vk::PipelineLayoutCreateInfo::default()
.set_layouts(&set_layouts)
.push_constant_ranges(std::slice::from_ref(&push_range));
device
.create_pipeline_layout(&info)
.map_err(|e| super::error::map_vk_result(e, "particle compute pipeline layout"))
}
fn create_render_pipeline_layout(
device: &VkDevice,
view_set_layout: vk::DescriptorSetLayout,
emitter_set_layout: vk::DescriptorSetLayout,
) -> RenderResult<OwnedPipelineLayout> {
let push_range = vk::PushConstantRange::default()
.stage_flags(vk::ShaderStageFlags::VERTEX)
.offset(0)
.size(PARTICLE_PUSH_BYTES);
let set_layouts = [view_set_layout, emitter_set_layout];
let info = vk::PipelineLayoutCreateInfo::default()
.set_layouts(&set_layouts)
.push_constant_ranges(std::slice::from_ref(&push_range));
device
.create_pipeline_layout(&info)
.map_err(|e| super::error::map_vk_result(e, "particle render pipeline layout"))
}
fn create_descriptor_pool(device: &VkDevice, frames: usize) -> RenderResult<OwnedDescriptorPool> {
let frames = frames as u32;
let max_emitters = MAX_EMITTERS as u32;
let sizes = PoolSizes::default()
.sets(&view_set_bindings(), frames)
.sets(&compute_set_bindings(), max_emitters)
.sets(&emitter_set_bindings(), max_emitters)
.build();
let info = vk::DescriptorPoolCreateInfo::default()
.max_sets(frames + 2 * max_emitters)
.pool_sizes(&sizes);
device
.create_descriptor_pool(&info)
.map_err(|e| super::error::map_vk_result(e, "particle descriptor pool"))
}
fn frame_view(views: &[vk::ImageView], i: usize) -> vk::ImageView {
views[i.min(views.len().saturating_sub(1))]
}
fn write_depth_binding(device: &VkDevice, set: vk::DescriptorSet, depth_view: vk::ImageView) {
SetWrites::new(set)
.sampled_image(1, depth_view)
.apply(device);
}
fn create_sampler(device: &VkDevice) -> RenderResult<OwnedSampler> {
let info = vk::SamplerCreateInfo::default()
.mag_filter(vk::Filter::LINEAR)
.min_filter(vk::Filter::LINEAR)
.mipmap_mode(vk::SamplerMipmapMode::LINEAR)
.address_mode_u(vk::SamplerAddressMode::CLAMP_TO_EDGE)
.address_mode_v(vk::SamplerAddressMode::CLAMP_TO_EDGE)
.address_mode_w(vk::SamplerAddressMode::CLAMP_TO_EDGE)
.border_color(vk::BorderColor::FLOAT_OPAQUE_BLACK)
.max_lod(vk::LOD_CLAMP_NONE);
device
.create_sampler(&info)
.map_err(|e| super::error::map_vk_result(e, "particle sampler"))
}
fn create_render_pipeline(
device: &VkDevice,
render_pass: vk::RenderPass,
layout: vk::PipelineLayout,
vert_spv: &[u8],
frag_spv: &[u8],
) -> RenderResult<OwnedPipeline> {
GraphicsPipelineDesc {
topology: vk::PrimitiveTopology::TRIANGLE_STRIP,
..GraphicsPipelineDesc::fullscreen(
vert_spv,
frag_spv,
layout,
render_pass,
device.writer_targets().blends(),
)
}
.build(device, "particle render")
}
fn zero_device_buffer(gpu: GpuUploadContext, target: vk::Buffer, bytes: u64) -> RenderResult<()> {
let GpuUploadContext {
device,
command_pool,
queue,
..
} = gpu;
super::texture::one_shot_submit(device, command_pool, queue, |cmd| {
unsafe { device.cmd_fill_buffer(cmd, target, 0, bytes, 0) };
})
}
impl VkContext {
pub(in crate::vulkan) fn prepare_particle_pass(
&mut self,
elapsed: f32,
) -> Option<ParticleFrame> {
self.particle.resources.as_ref()?;
if self.particle.records.is_empty() || self.particle.emitter_state.is_empty() {
return None;
}
let dt = (elapsed - self.particle.last_elapsed.get()).max(0.0);
self.particle.last_elapsed.set(elapsed);
let frame_index = self.particle.frame_index.get().wrapping_add(1);
self.particle.frame_index.set(frame_index);
let spawns = self
.particle
.records
.iter()
.zip(self.particle.emitter_state.iter())
.map(
|(rec_slot, gpu_slot)| match (rec_slot.as_ref(), gpu_slot.as_ref()) {
(Some(rec), Some(gpu)) => gpu.spawn_state.borrow_mut().take_spawns(dt, rec),
_ => ParticleSpawns::default(),
},
)
.collect();
Some(ParticleFrame {
dt,
frame_index,
spawns,
})
}
fn particle_params(&self, frame: &ParticleFrame) -> Vec<Option<ParticleParams>> {
self.particle
.records
.iter()
.zip(self.particle.emitter_state.iter())
.enumerate()
.map(|(i, (rec_slot, gpu_slot))| {
let rec = match (rec_slot.as_ref(), gpu_slot.as_ref()) {
(Some(r), Some(_)) => r,
_ => return None,
};
let spawns = frame.spawns.get(i).copied().unwrap_or_default();
Some(rec.params(frame.dt, spawns, spawn_seed(frame.frame_index, i)))
})
.collect()
}
pub(in crate::vulkan) fn encode_particles_sim(
&self,
cmd: vk::CommandBuffer,
frame: &ParticleFrame,
) {
let Some(resources) = self.particle.resources.as_ref() else {
return;
};
if self.particle.records.is_empty() || self.particle.emitter_state.is_empty() {
return;
}
let device = &self.hw.device;
let params_per_emitter = self.particle_params(frame);
unsafe {
device.cmd_bind_pipeline(
cmd,
vk::PipelineBindPoint::COMPUTE,
resources.compute_pipeline.handle(),
);
}
for (i, data) in params_per_emitter.iter().enumerate() {
let Some(params) = data.as_ref() else {
continue;
};
let Some(gpu) = self.particle.emitter_state[i].as_ref() else {
continue;
};
let Some(rec) = self.particle.records[i].as_ref() else {
continue;
};
unsafe {
device.cmd_bind_descriptor_sets(
cmd,
vk::PipelineBindPoint::COMPUTE,
resources.compute_pipeline_layout.handle(),
0,
std::slice::from_ref(&gpu.compute_set),
&[],
);
cmd_push_constants(
device,
cmd,
resources.compute_pipeline_layout.handle(),
vk::ShaderStageFlags::COMPUTE,
params,
);
let groups = rec.max_particles.div_ceil(64);
device.cmd_dispatch(cmd, groups, 1, 1);
}
}
}
pub(in crate::vulkan) fn encode_particles_draw(
&self,
cmd: vk::CommandBuffer,
frame_idx: usize,
frame: &ParticleFrame,
vp: [[f32; 4]; 4],
frustum: &Frustum,
mask: ReactiveWrite,
) -> bool {
let Some(resources) = self.particle.resources.as_ref() else {
return false;
};
if self.particle.records.is_empty() || self.particle.emitter_state.is_empty() {
return false;
}
let device = &self.hw.device;
let extent = self.targets.render_extent;
let visible: Vec<bool> = self
.particle
.records
.iter()
.map(|slot| match slot {
Some(r) => {
let (mn, mx) = r.aabb();
frustum.intersects_aabb(mn, mx)
}
None => false,
})
.collect();
if !visible.iter().any(|v| *v) {
return false;
}
let params_per_emitter = self.particle_params(frame);
let v = self.state.view.matrix;
let cam_right = [v[0][0], v[1][0], v[2][0]];
let cam_up = [v[0][1], v[1][1], v[2][1]];
let view_uni = ParticleView {
vp,
cam_right,
_pad0: 0.0,
cam_up,
_pad1: 0.0,
};
resources.view_ubos[frame_idx].write_val(0, &view_uni);
let clears = [reactive_mask::REACTIVE_MASK_CLEAR; 2];
let rp_begin = vk::RenderPassBeginInfo::default()
.render_pass(resources.render_passes.get(mask))
.framebuffer(resources.framebuffers[frame_idx].handle())
.render_area(vk::Rect2D::default().extent(extent))
.clear_values(&clears);
let viewport = vk::Viewport {
x: 0.0,
y: extent.height as f32,
width: extent.width as f32,
height: -(extent.height as f32),
min_depth: 0.0,
max_depth: 1.0,
};
let scissor = vk::Rect2D::default().extent(extent);
unsafe {
device.cmd_begin_render_pass(cmd, &rp_begin, vk::SubpassContents::INLINE);
device.cmd_set_viewport(cmd, 0, std::slice::from_ref(&viewport));
device.cmd_set_scissor(cmd, 0, std::slice::from_ref(&scissor));
device.cmd_bind_pipeline(
cmd,
vk::PipelineBindPoint::GRAPHICS,
resources.render_pipeline.handle(),
);
device.cmd_bind_descriptor_sets(
cmd,
vk::PipelineBindPoint::GRAPHICS,
resources.render_pipeline_layout.handle(),
0,
std::slice::from_ref(&resources.view_sets[frame_idx]),
&[],
);
}
for (i, data) in params_per_emitter.iter().enumerate() {
if !visible[i] {
continue;
}
let Some(params) = data.as_ref() else {
continue;
};
let Some(gpu) = self.particle.emitter_state[i].as_ref() else {
continue;
};
let Some(rec) = self.particle.records[i].as_ref() else {
continue;
};
unsafe {
device.cmd_bind_descriptor_sets(
cmd,
vk::PipelineBindPoint::GRAPHICS,
resources.render_pipeline_layout.handle(),
1,
std::slice::from_ref(&gpu.render_set),
&[],
);
cmd_push_constants(
device,
cmd,
resources.render_pipeline_layout.handle(),
vk::ShaderStageFlags::VERTEX,
params,
);
device.cmd_draw(cmd, 4, rec.max_particles, 0, 0);
}
self.inc_draw_calls(1);
}
unsafe {
device.cmd_end_render_pass(cmd);
}
true
}
}
impl VkContext {
pub(in crate::vulkan) fn add_particle_emitter(
&mut self,
record: ParticleEmitterRecord,
) -> RenderResult<usize> {
if self.particle.resources.is_none() {
let hdr_resolve_views: Vec<vk::ImageView> = self
.targets
.hdr_resolve_images
.iter()
.map(|img| img.view)
.collect();
let depth_views: Vec<vk::ImageView> = self
.targets
.depth_images
.iter()
.map(|img| img.view)
.collect();
let reactive_mask_views: Vec<vk::ImageView> = self
.targets
.reactive_mask_images
.iter()
.map(|img| img.view)
.collect();
let resources = ParticleResources::new(
&GpuUploadContext {
alloc: &self.hw.alloc,
device: &self.hw.device,
command_pool: self.commands.command_pool,
queue: self.hw.graphics_queue,
},
self.frames_in_flight,
ParticlePassTargets {
hdr_resolve_views: &hdr_resolve_views,
reactive_mask_views: &reactive_mask_views,
depth_views: &depth_views,
extent: self.targets.render_extent,
},
self.targets.msaa_samples != vk::SampleCountFlags::TYPE_1,
self.hot_reload.enabled,
)?;
self.particle.resources = Some(resources);
}
let live_count = self.particle.records.iter().filter(|s| s.is_some()).count();
if live_count >= MAX_EMITTERS {
return Err(RenderError::Other(format!(
"add_emitter: MAX_EMITTERS ({MAX_EMITTERS}) exceeded"
)));
}
let gpu_state = build_emitter_gpu_state(
GpuUploadContext {
alloc: &self.hw.alloc,
device: &self.hw.device,
command_pool: self.commands.command_pool,
queue: self.hw.graphics_queue,
},
self.particle
.resources
.as_ref()
.expect("particle resources are live"),
&record,
)?;
let last_tex = self.scene.textures.len().saturating_sub(1);
let tex_idx = record.texture_slot.min(last_tex);
write_render_albedo_binding(
&self.hw.device,
gpu_state.render_set,
self.scene.textures[tex_idx].view,
);
let id = if let Some(slot) = self.particle.free_slots.pop() {
self.particle.records[slot] = Some(record);
let new_state = ParticleEmitterGpuState {
texture_slot: tex_idx,
..gpu_state
};
self.particle.emitter_state[slot] = Some(new_state);
slot
} else {
let new_state = ParticleEmitterGpuState {
texture_slot: tex_idx,
..gpu_state
};
self.particle.records.push(Some(record));
self.particle.emitter_state.push(Some(new_state));
self.particle.records.len() - 1
};
Ok(id)
}
pub(in crate::vulkan) fn remove_particle_emitter(
&mut self,
emitter_id: usize,
) -> RenderResult<()> {
let rec_slot = self.particle.records.get_mut(emitter_id).ok_or_else(|| {
RenderError::Other(format!("remove_emitter: id {emitter_id} out of range"))
})?;
if rec_slot.is_none() {
return Err(RenderError::Other(format!(
"remove_emitter: id {emitter_id} already removed"
)));
}
*rec_slot = None;
if let Some(gpu_slot) = self.particle.emitter_state.get_mut(emitter_id)
&& let Some(state) = gpu_slot.take()
{
self.wait_idle();
drop(state);
}
self.particle.free_slots.push(emitter_id);
Ok(())
}
pub(in crate::vulkan) fn upload_initial_particles(
&mut self,
records: Vec<ParticleEmitterRecord>,
) -> RenderResult<()> {
if records.is_empty() {
return Ok(());
}
if records.len() > MAX_EMITTERS {
return Err(RenderError::Other(format!(
"particles: {} authored emitters exceed MAX_EMITTERS ({})",
records.len(),
MAX_EMITTERS
)));
}
for record in records {
self.add_particle_emitter(record)?;
}
Ok(())
}
pub(in crate::vulkan) fn particle_samples_slot(&self, slot: usize) -> bool {
let last = self.scene.textures.len().saturating_sub(1);
self.particle
.emitter_state
.iter()
.flatten()
.any(|state| state.texture_slot.min(last) == slot)
}
pub(in crate::vulkan) fn rewrite_particle_albedo_slot(&self, slot: usize) {
if self.particle.resources.is_none() {
return;
}
let last = self.scene.textures.len().saturating_sub(1);
let view = self.scene.textures[slot].view;
for state in self.particle.emitter_state.iter().flatten() {
if state.texture_slot.min(last) == slot {
write_render_albedo_binding(&self.hw.device, state.render_set, view);
}
}
}
pub(in crate::vulkan) fn destroy_particle_emitter_states(&mut self, _device: &VkDevice) {
self.particle.emitter_state.clear();
}
}
fn write_render_albedo_binding(device: &VkDevice, set: vk::DescriptorSet, view: vk::ImageView) {
SetWrites::new(set).sampled_image(1, view).apply(device);
}