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, spawn_seed};
use concinnity_core::render::reactive_mask::ReactiveWrite;
use windows::Win32::Foundation::RECT;
use windows::Win32::Graphics::Direct3D12::*;
use super::allocator::{DeviceAllocator, PooledBuffer};
use crate::directx::builtin_shaders;
use crate::directx::builtin_shaders::CompileProgram;
use crate::directx::com;
use crate::directx::context::{DxContext, FRAMES, align256, dump_on_err};
use crate::directx::descriptor_slot::DescriptorTables;
use crate::directx::descriptor_slot::SrvSlot;
use crate::directx::error::map_hresult;
use crate::directx::pso::{Blend, GraphicsPso, Raster, compute_pso};
use crate::directx::reactive_mask::mask_target;
use crate::directx::root_sig::{RootSig, SamplerState, Visibility};
use crate::directx::texture::{
HDR_FORMAT, create_uav_buffer, transition_barrier, write_texture_srv,
};
pub(in crate::directx) struct ParticleState {
pub resources: Option<ParticleResources>,
pub records: Vec<Option<ParticleEmitterRecord>>,
pub emitter_state: Vec<Option<ParticleEmitterGpuState>>,
pub free_slots: Vec<usize>,
pub srv_base_slot: usize,
pub last_elapsed: std::cell::Cell<f32>,
pub frame_index: std::cell::Cell<u32>,
}
pub(in crate::directx) const MAX_EMITTERS: usize = 256;
pub(in crate::directx) use concinnity_core::render::uniforms::GpuParticle;
pub(in crate::directx) use concinnity_core::render::uniforms::ParticleView;
type ParticleShaders = (Vec<u8>, Vec<u8>, Vec<u8>);
pub(in crate::directx) fn compile_particle_shaders(
msaa_samples: u32,
hot_reload: bool,
) -> RenderResult<ParticleShaders> {
let cs = builtin_shaders::PARTICLE_SIMULATE.compile(hot_reload)?;
let vs = builtin_shaders::PARTICLE_VERT.compile(hot_reload)?;
let ps = builtin_shaders::PARTICLE_FRAG
.at(msaa_samples > 1)
.compile(hot_reload)?;
Ok((cs, vs, ps))
}
pub(in crate::directx) struct ParticleEmitterGpuState {
pub pool: ID3D12Resource,
pub spawn_state: std::cell::RefCell<ParticleSpawnState>,
}
fn create_simulate_root_signature(device: &ID3D12Device) -> RenderResult<ID3D12RootSignature> {
RootSig::new()
.cbv(0, Visibility::All)
.uav(0, Visibility::All)
.build(device, "particle simulate root sig")
}
fn create_render_root_signature(device: &ID3D12Device) -> RenderResult<ID3D12RootSignature> {
RootSig::new()
.cbv(0, Visibility::Vertex)
.cbv(1, Visibility::All)
.srv(0, Visibility::Vertex)
.srv_table(1, 1, Visibility::Pixel)
.srv_table(2, 1, Visibility::Pixel)
.static_sampler(SamplerState::LinearClamp, 0, Visibility::Pixel)
.build(device, "particle render root sig")
}
fn create_simulate_pso(
device: &ID3D12Device,
root_sig: &ID3D12RootSignature,
cs: &[u8],
) -> RenderResult<ID3D12PipelineState> {
compute_pso(device, root_sig, cs, "particle simulate")
}
fn create_render_pso(
device: &ID3D12Device,
root_sig: &ID3D12RootSignature,
vs: &[u8],
ps: &[u8],
) -> RenderResult<ID3D12PipelineState> {
mask_target(GraphicsPso::new(root_sig, vs, ps).target(HDR_FORMAT, Blend::AlphaOver))
.raster(Raster {
depth_clip: false,
..Raster::default()
})
.build(device, "particle render")
}
pub(in crate::directx) struct ParticleResources {
pub(in crate::directx) simulate_root_sig: ID3D12RootSignature,
pub(in crate::directx) simulate_pso: ID3D12PipelineState,
pub(in crate::directx) render_root_sig: ID3D12RootSignature,
pub(in crate::directx) render_pso: ID3D12PipelineState,
pub(in crate::directx) view_ubo_resources: Vec<PooledBuffer>,
pub(in crate::directx) view_ubo_ptrs: Vec<*mut u8>,
pub(in crate::directx) params_ubo_resources: Vec<PooledBuffer>,
pub(in crate::directx) params_ubo_ptrs: Vec<*mut u8>,
pub(in crate::directx) params_stride: u64,
pub(in crate::directx) emitter_srv_base_slot: usize,
depth_srv_gpu: SrvSlot,
}
impl ParticleResources {
pub(in crate::directx) fn new(
alloc: &DeviceAllocator,
emitter_srv_base_slot: usize,
msaa_samples: u32,
depth_srv_gpu: SrvSlot,
info_queue: Option<&ID3D12InfoQueue>,
hot_reload: bool,
) -> RenderResult<Self> {
let device = alloc.device();
let (cs, vs, ps) = compile_particle_shaders(msaa_samples, hot_reload)?;
let simulate_root_sig = dump_on_err(info_queue, create_simulate_root_signature(device))?;
let simulate_pso = dump_on_err(
info_queue,
create_simulate_pso(device, &simulate_root_sig, &cs),
)?;
let render_root_sig = dump_on_err(info_queue, create_render_root_signature(device))?;
let render_pso = dump_on_err(
info_queue,
create_render_pso(device, &render_root_sig, &vs, &ps),
)?;
let view_size = align256(std::mem::size_of::<ParticleView>() as u64);
let mut view_ubo_resources: Vec<PooledBuffer> = Vec::with_capacity(FRAMES);
let mut view_ubo_ptrs: Vec<*mut u8> = Vec::with_capacity(FRAMES);
for _ in 0..FRAMES {
let buf = alloc.alloc_buffer(
view_size,
D3D12_HEAP_TYPE_UPLOAD,
D3D12_RESOURCE_STATE_GENERIC_READ,
)?;
let mut ptr = std::ptr::null_mut::<std::ffi::c_void>();
unsafe { buf.Map(0, None, Some(&mut ptr)) }
.map_err(|e| map_hresult(e.code(), "map particle view ubo"))?;
view_ubo_ptrs.push(ptr as *mut u8);
view_ubo_resources.push(buf);
}
let params_stride = align256(std::mem::size_of::<ParticleParams>() as u64);
let params_total = params_stride * MAX_EMITTERS as u64;
let mut params_ubo_resources: Vec<PooledBuffer> = Vec::with_capacity(FRAMES);
let mut params_ubo_ptrs: Vec<*mut u8> = Vec::with_capacity(FRAMES);
for _ in 0..FRAMES {
let buf = alloc.alloc_buffer(
params_total,
D3D12_HEAP_TYPE_UPLOAD,
D3D12_RESOURCE_STATE_GENERIC_READ,
)?;
let mut ptr = std::ptr::null_mut::<std::ffi::c_void>();
unsafe { buf.Map(0, None, Some(&mut ptr)) }
.map_err(|e| map_hresult(e.code(), "map particle params ubo"))?;
params_ubo_ptrs.push(ptr as *mut u8);
params_ubo_resources.push(buf);
}
Ok(Self {
simulate_root_sig,
simulate_pso,
render_root_sig,
render_pso,
view_ubo_resources,
view_ubo_ptrs,
params_ubo_resources,
params_ubo_ptrs,
params_stride,
emitter_srv_base_slot,
depth_srv_gpu,
})
}
}
pub(in crate::directx) fn build_emitter_gpu_state(
alloc: &DeviceAllocator,
record: &ParticleEmitterRecord,
) -> RenderResult<ParticleEmitterGpuState> {
let device = alloc.device();
let slots = record.max_particles as u64;
let pool_bytes = slots * std::mem::size_of::<GpuParticle>() as u64;
let pool = create_uav_buffer(device, pool_bytes, D3D12_RESOURCE_STATE_COMMON)?;
zero_default_buffer(alloc, &pool, pool_bytes)?;
Ok(ParticleEmitterGpuState {
pool,
spawn_state: std::cell::RefCell::new(ParticleSpawnState::default()),
})
}
fn zero_default_buffer(
alloc: &DeviceAllocator,
target: &ID3D12Resource,
bytes: u64,
) -> RenderResult<()> {
let device = alloc.device();
let upload = alloc.alloc_buffer(
bytes,
D3D12_HEAP_TYPE_UPLOAD,
D3D12_RESOURCE_STATE_GENERIC_READ,
)?;
let mut ptr = std::ptr::null_mut::<std::ffi::c_void>();
unsafe { upload.Map(0, None, Some(&mut ptr)) }
.map_err(|e| map_hresult(e.code(), "zero_default_buffer: map upload"))?;
unsafe { std::ptr::write_bytes(ptr as *mut u8, 0, bytes as usize) };
unsafe { upload.Unmap(0, None) };
let cmd_alloc: ID3D12CommandAllocator =
unsafe { device.CreateCommandAllocator(D3D12_COMMAND_LIST_TYPE_DIRECT) }
.map_err(|e| map_hresult(e.code(), "zero_default_buffer: alloc"))?;
let list: ID3D12GraphicsCommandList =
unsafe { device.CreateCommandList(0, D3D12_COMMAND_LIST_TYPE_DIRECT, &cmd_alloc, None) }
.map_err(|e| map_hresult(e.code(), "zero_default_buffer: list"))?;
let to_copy_dest = transition_barrier(
target,
D3D12_RESOURCE_STATE_COMMON,
D3D12_RESOURCE_STATE_COPY_DEST,
);
unsafe {
list.ResourceBarrier(&[to_copy_dest]);
list.CopyBufferRegion(target, 0, &*upload, 0, bytes);
}
let back_to_uav = transition_barrier(
target,
D3D12_RESOURCE_STATE_COPY_DEST,
D3D12_RESOURCE_STATE_UNORDERED_ACCESS,
);
unsafe { list.ResourceBarrier(&[back_to_uav]) };
unsafe { list.Close() }.map_err(|e| map_hresult(e.code(), "zero_default_buffer: close"))?;
let cmd: ID3D12CommandList = windows::core::Interface::cast(&list)
.map_err(|e| map_hresult(e.code(), "zero_default_buffer: cast"))?;
unsafe { alloc.queue().ExecuteCommandLists(&[Some(cmd)]) };
let fence: ID3D12Fence = unsafe { device.CreateFence(0, D3D12_FENCE_FLAG_NONE) }
.map_err(|e| map_hresult(e.code(), "zero_default_buffer: fence"))?;
unsafe { alloc.queue().Signal(&fence, 1) }
.map_err(|e| map_hresult(e.code(), "zero_default_buffer: signal"))?;
if unsafe { fence.GetCompletedValue() } < 1 {
let event =
unsafe { windows::Win32::System::Threading::CreateEventW(None, false, false, None) }
.map_err(|e| map_hresult(e.code(), "zero_default_buffer: event"))?;
unsafe { fence.SetEventOnCompletion(1, event) }
.map_err(|e| map_hresult(e.code(), "zero_default_buffer: set event"))?;
unsafe { windows::Win32::System::Threading::WaitForSingleObject(event, u32::MAX) };
unsafe { windows::Win32::Foundation::CloseHandle(event) }.ok();
}
Ok(())
}
struct EmitterFrameData {
params_gva: u64,
pool_gva: u64,
}
pub(in crate::directx) struct ParticleFrame {
emitters: Vec<Option<EmitterFrameData>>,
}
impl DxContext {
pub(in crate::directx) fn emitter_albedo_srv_gpu(&self, i: usize) -> SrvSlot {
let base = self
.particle
.resources
.as_ref()
.map(|s| s.emitter_srv_base_slot)
.unwrap_or(0);
SrvSlot::at(
&self.descriptors.srv_heap,
self.descriptors.srv_descriptor_size,
base + i,
)
}
pub(in crate::directx) fn prepare_particle_pass(
&self,
frame_idx: usize,
elapsed: f32,
) -> Option<ParticleFrame> {
let resources = 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 params_base_gva = com::gpu_va(&resources.params_ubo_resources[frame_idx]);
let mut emitters: Vec<Option<EmitterFrameData>> =
Vec::with_capacity(self.particle.records.len());
for (i, (rec_slot, gpu_slot)) in self
.particle
.records
.iter()
.zip(self.particle.emitter_state.iter())
.enumerate()
{
let (rec, gpu) = match (rec_slot.as_ref(), gpu_slot.as_ref()) {
(Some(r), Some(g)) => (r, g),
_ => {
emitters.push(None);
continue;
}
};
let spawns = gpu.spawn_state.borrow_mut().take_spawns(dt, rec);
let params = rec.params(dt, spawns, spawn_seed(frame_index, i));
unsafe {
let dst = resources.params_ubo_ptrs[frame_idx]
.add((i as u64 * resources.params_stride) as usize);
std::ptr::copy_nonoverlapping(
¶ms as *const ParticleParams as *const u8,
dst,
std::mem::size_of::<ParticleParams>(),
);
}
emitters.push(Some(EmitterFrameData {
params_gva: params_base_gva + i as u64 * resources.params_stride,
pool_gva: com::gpu_va(&gpu.pool),
}));
}
Some(ParticleFrame { emitters })
}
pub(in crate::directx) fn encode_particles_sim(
&self,
cmd: &ID3D12GraphicsCommandList,
frame: &ParticleFrame,
) {
let Some(resources) = self.particle.resources.as_ref() else {
return;
};
let frame_data = frame.emitters.as_slice();
unsafe {
cmd.SetComputeRootSignature(&resources.simulate_root_sig);
cmd.SetPipelineState(&resources.simulate_pso);
}
for (i, data) in frame_data.iter().enumerate() {
let Some(data) = data else {
continue;
};
let Some(rec) = self.particle.records[i].as_ref() else {
continue;
};
unsafe {
cmd.SetComputeRootConstantBufferView(0, data.params_gva);
cmd.SetComputeRootUnorderedAccessView(1, data.pool_gva);
let groups = rec.max_particles.div_ceil(64);
cmd.Dispatch(groups, 1, 1);
}
}
}
pub(in crate::directx) fn encode_particles_draw(
&self,
cmd: &ID3D12GraphicsCommandList,
frame_idx: usize,
frame: &ParticleFrame,
vp: [[f32; 4]; 4],
frustum: &Frustum,
reactive: ReactiveWrite,
) {
let Some(resources) = self.particle.resources.as_ref() else {
return;
};
let frame_data = frame.emitters.as_slice();
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();
let any_visible = visible.iter().any(|v| *v);
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,
};
unsafe {
std::ptr::copy_nonoverlapping(
&view_uni as *const ParticleView as *const u8,
resources.view_ubo_ptrs[frame_idx],
std::mem::size_of::<ParticleView>(),
);
}
let view_gva = com::gpu_va(&resources.view_ubo_resources[frame_idx]);
if any_visible {
let rtvs = [
self.hdr_scene_rtv(),
self.targets.reactive_mask.rtv(reactive),
];
let w = self.targets.extent.render_width;
let h = self.targets.extent.render_height;
unsafe {
cmd.OMSetRenderTargets(2, Some(rtvs.as_ptr()), false, None);
let viewport = D3D12_VIEWPORT {
TopLeftX: 0.0,
TopLeftY: 0.0,
Width: w as f32,
Height: h as f32,
MinDepth: 0.0,
MaxDepth: 1.0,
};
cmd.RSSetViewports(&[viewport]);
let scissor = RECT {
left: 0,
top: 0,
right: w as i32,
bottom: h as i32,
};
cmd.RSSetScissorRects(&[scissor]);
cmd.IASetPrimitiveTopology(
windows::Win32::Graphics::Direct3D::D3D_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP,
);
cmd.SetPipelineState(&resources.render_pso);
cmd.SetGraphicsRootSignature(&resources.render_root_sig);
cmd.SetDescriptorHeaps(&[Some(self.descriptors.srv_heap.clone())]);
cmd.SetGraphicsRootConstantBufferView(0, view_gva);
cmd.set_graphics_srv_table(4, resources.depth_srv_gpu);
}
for (i, data) in frame_data.iter().enumerate() {
if !visible[i] {
continue;
}
let Some(data) = data else {
continue;
};
let Some(rec) = self.particle.records[i].as_ref() else {
continue;
};
let albedo_srv_gpu = self.emitter_albedo_srv_gpu(i);
unsafe {
cmd.SetGraphicsRootConstantBufferView(1, data.params_gva);
cmd.SetGraphicsRootShaderResourceView(2, data.pool_gva);
cmd.set_graphics_srv_table(3, albedo_srv_gpu);
cmd.DrawInstanced(4, rec.max_particles, 0, 0);
}
self.inc_draw_calls(1);
}
}
}
}
impl DxContext {
pub(crate) fn add_emitter(&mut self, record: ParticleEmitterRecord) -> RenderResult<usize> {
if self.particle.resources.is_none() {
let resources = ParticleResources::new(
&self.hw.alloc,
self.particle.srv_base_slot,
self.targets.hdr.msaa_samples,
self.targets.main_depth_srv_gpu,
self.hw.info_queue.as_ref(),
self.hot_reload.enabled,
)?;
self.particle.resources = Some(resources);
}
let base_slot = self
.particle
.resources
.as_ref()
.map(|r| r.emitter_srv_base_slot)
.ok_or_else(|| {
RenderError::Other("add_emitter: particle pipeline unavailable".to_string())
})?;
let gpu_state = build_emitter_gpu_state(&self.hw.alloc, &record)?;
let last_tex = self.scene.textures.len().saturating_sub(1);
let tex_idx = record.texture_slot.min(last_tex);
let id = if let Some(slot) = self.particle.free_slots.pop() {
self.particle.records[slot] = Some(record);
self.particle.emitter_state[slot] = Some(gpu_state);
slot
} else {
if self.particle.records.len() >= MAX_EMITTERS {
return Err(RenderError::Other(format!(
"add_emitter: MAX_EMITTERS ({MAX_EMITTERS}) exceeded"
)));
}
self.particle.records.push(Some(record));
self.particle.emitter_state.push(Some(gpu_state));
self.particle.records.len() - 1
};
let srv_cpu = D3D12_CPU_DESCRIPTOR_HANDLE {
ptr: unsafe {
self.descriptors
.srv_heap
.GetCPUDescriptorHandleForHeapStart()
}
.ptr + (base_slot + id) * self.descriptors.srv_descriptor_size,
};
write_texture_srv(&self.hw.device, &self.scene.textures[tex_idx], srv_cpu);
Ok(id)
}
pub(crate) fn remove_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) {
*gpu_slot = None;
}
self.particle.free_slots.push(emitter_id);
Ok(())
}
}