effectcraft/crates/gpu/src/particles.rs

245 lines
9.7 KiB
Rust

//! GPU particles (`particles.wgsl`): the stepped particle systems of `effectcraft-effects`
//! ([`effectcraft_effects::psim`]) simulated with one invocation per particle. States stay on
//! the GPU as checkpoints (per system key, like the CPU's `SimCache`), so playback advances from
//! the previous frame's state instead of simulating from layer time 0.
use std::sync::{Mutex, OnceLock};
use effectcraft_effects::psim::{ParticleSystem, SimParticle, SimRequest};
use crate::context::GpuContext;
/// Bytes of one particle state (`PState` in the shader).
const STATE_BYTES: u64 = 48;
/// Checkpoints kept (all keys).
const CHECKPOINTS: usize = 24;
pub(crate) struct Pipes {
bgl: wgpu::BindGroupLayout,
pipe: wgpu::ComputePipeline,
}
struct Checkpoint {
key: u64,
steps: u64,
n: u32,
buf: wgpu::Buffer,
used: u64,
}
#[derive(Default)]
pub(crate) struct Checkpoints {
list: Vec<Checkpoint>,
clock: u64,
}
fn pipes(g: &GpuContext) -> &Pipes {
g.particles.get_or_init(|| {
let d = &g.device;
let src = [include_str!("shaders/common.wgsl"), include_str!("shaders/kernels.wgsl"), include_str!("shaders/particles.wgsl")].concat();
let module =
d.create_shader_module(wgpu::ShaderModuleDescriptor { label: Some("effectcraft particles"), source: wgpu::ShaderSource::Wgsl(src.into()) });
let buffer = |binding: u32, ty: wgpu::BufferBindingType| wgpu::BindGroupLayoutEntry {
binding,
visibility: wgpu::ShaderStages::COMPUTE,
ty: wgpu::BindingType::Buffer { ty, has_dynamic_offset: false, min_binding_size: None },
count: None,
};
let bgl = d.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("effectcraft particles"),
entries: &[
buffer(0, wgpu::BufferBindingType::Uniform),
buffer(1, wgpu::BufferBindingType::Storage { read_only: true }),
buffer(2, wgpu::BufferBindingType::Storage { read_only: true }),
buffer(3, wgpu::BufferBindingType::Storage { read_only: false }),
],
});
let layout = d.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("effectcraft particles"),
bind_group_layouts: &[None, Some(&bgl)],
immediate_size: 0,
});
let pipe = d.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
label: Some("particles"),
layout: Some(&layout),
module: &module,
entry_point: Some("particles"),
compilation_options: Default::default(),
cache: None,
});
Pipes { bgl, pipe }
})
}
/// The shader's parameter block.
fn uniforms(req: &SimRequest, n: u32, s0: u64, prev_n: u32) -> Vec<u8> {
let dt = (1.0 / effectcraft_effects::psim::SPS) as f32;
let mut u = [n, s0 as u32, req.steps as u32, 0];
let u2;
let mut f = [[0.0f32; 4]; 6];
match &req.system {
ParticleSystem::Engine(e) => {
u2 = [e.anim, e.flat as u32, e.seed, prev_n];
f[0] = [e.life, e.speed, e.extra, e.cone];
f[1] = [e.producer[0], e.producer[1], e.producer[2], 0.0];
f[2] = [e.radius[0], e.radius[1], e.radius[2], 0.0];
f[3] = [e.gravity[0], e.gravity[1], e.gravity[2], 0.0];
f[4] = [e.axis[0], e.axis[1], e.axis[2], 0.0];
f[5] = [e.damp, dt, 0.0, 0.0];
}
ParticleSystem::Cannon(c) => {
u[3] = 1;
u2 = [0, 1, c.seed, prev_n];
f[0] = [c.pos[0], c.pos[1], c.barrel_angle, c.barrel_radius];
f[1] = [c.dir_spread, c.vel, c.vel_spread, c.grav_spread];
f[2] = [c.gravity[0], c.gravity[1], dt, 0.0];
f[3] = c.bounds;
}
}
let mut out = Vec::with_capacity(128);
for x in u.iter().chain(&u2) {
out.extend_from_slice(&x.to_le_bytes());
}
for x in f.iter().flatten() {
out.extend_from_slice(&x.to_le_bytes());
}
out
}
/// Simulate `req` (see [`effectcraft_effects::psim::ParticleSim`]).
///
/// With deferred readbacks (a browser worker) the state's readback is keyed by the request
/// ([`crate::deferred`]): the first pass starts it and gets no particles (the pass misses and is
/// rendered again), later passes read the bytes.
pub(crate) fn simulate(g: &GpuContext, req: &SimRequest) -> Option<Vec<SimParticle>> {
if !g.can_readback() || req.steps > u32::MAX as u64 {
return None;
}
let n = req.births.len() as u32;
if n == 0 {
return Some(vec![]);
}
let deferred = g.deferred.clone();
let key = deferred.as_ref().map(|_| sim_key(req));
if let (Some(d), Some(k)) = (&deferred, key) {
match d.lookup(k) {
crate::deferred::Lookup::Ready(r) => return r.map(|r| parse(&r.bytes)),
crate::deferred::Lookup::InFlight => {
d.miss();
return Some(vec![]);
}
crate::deferred::Lookup::Absent => {}
}
}
let size = n as u64 * STATE_BYTES;
if size > g.device.limits().max_storage_buffer_binding_size {
return None;
}
let p = pipes(g);
// The latest checkpoint at or before the target step.
let (s0, prev, prev_n) = {
let mut c = g.particle_states.lock().ok()?;
c.clock += 1;
let now = c.clock;
match c.list.iter_mut().filter(|k| k.key == req.key && k.steps <= req.steps).max_by_key(|k| k.steps) {
Some(k) => {
k.used = now;
(k.steps, Some(k.buf.clone()), k.n)
}
None => (0, None, 0),
}
};
let d = &g.device;
let births: Vec<u8> = req.births.iter().flat_map(|b| b.to_le_bytes()).collect();
let births = g.buffer_init(Some("births"), &births, wgpu::BufferUsages::STORAGE);
let prev = prev.unwrap_or_else(|| {
d.create_buffer(&wgpu::BufferDescriptor { label: Some("particles"), size: STATE_BYTES, usage: wgpu::BufferUsages::STORAGE, mapped_at_creation: false })
});
let next = d.create_buffer(&wgpu::BufferDescriptor {
label: Some("particles"),
size,
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_SRC,
mapped_at_creation: false,
});
let ub = g.buffer_init(Some("particles"), &uniforms(req, n, s0, prev_n), wgpu::BufferUsages::UNIFORM);
let bg = d.create_bind_group(&wgpu::BindGroupDescriptor {
label: None,
layout: &p.bgl,
entries: &[
wgpu::BindGroupEntry { binding: 0, resource: ub.as_entire_binding() },
wgpu::BindGroupEntry { binding: 1, resource: births.as_entire_binding() },
wgpu::BindGroupEntry { binding: 2, resource: prev.as_entire_binding() },
wgpu::BindGroupEntry { binding: 3, resource: next.as_entire_binding() },
],
});
let read = d.create_buffer(&wgpu::BufferDescriptor {
label: Some("particles readback"),
size,
usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let mut enc = d.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("particles") });
{
let mut pass = enc.begin_compute_pass(&wgpu::ComputePassDescriptor { label: Some("particles"), timestamp_writes: None });
pass.set_pipeline(&p.pipe);
pass.set_bind_group(1, &bg, &[]);
pass.dispatch_workgroups(n.div_ceil(64), 1, 1);
}
enc.copy_buffer_to_buffer(&next, 0, &read, 0, size);
g.queue.submit([enc.finish()]);
if let (Some(d), Some(k)) = (&deferred, key) {
let row = usize::try_from(size).ok()?;
let done = d.start(k, n, 1, [0.0; 2], 1.0);
g.map_readback(&read, row, row, 1, move |r| done(r.map_err(|e| log::error!("gpu particle readback: {e}")).ok()));
#[cfg(not(target_arch = "wasm32"))]
let _ = g.poll_readbacks(wgpu::PollType::Poll);
keep_checkpoint(g, req, n, next);
d.miss();
return Some(vec![]);
}
let row = usize::try_from(size).ok()?;
let bytes = g.read_buffer(&read, row, row, 1).map_err(|e| log::error!("gpu particle readback: {e}")).ok()?;
let parts = parse(&bytes);
keep_checkpoint(g, req, n, next);
Some(parts)
}
/// The deferred readback key of a request: everything that determines the state.
fn sim_key(req: &SimRequest) -> u128 {
let mut k = crate::deferred::Key::new(3);
k.u64(req.key);
k.u64(req.steps);
k.u64(req.births.len() as u64);
k.bytes(&req.births.iter().flat_map(|b| b.to_le_bytes()).collect::<Vec<u8>>());
k.debug(&req.system);
k.finish()
}
/// Live particles of a state buffer's bytes.
fn parse(bytes: &[u8]) -> Vec<SimParticle> {
let f: Vec<f32> = bytes.as_chunks::<4>().0.iter().map(|c| f32::from_le_bytes([c[0], c[1], c[2], c[3]])).collect();
f.as_chunks::<12>()
.0
.iter()
.enumerate()
.filter(|(_, s)| s[9] != 0.0)
.map(|(id, s)| SimParticle { p: [s[0], s[1], s[2]], v: [s[4], s[5], s[6]], age: s[3], life: s[7], rnd: s[8], id: id as u32 })
.collect()
}
/// Keep a simulated state on the GPU as a checkpoint (the oldest go beyond the limit).
fn keep_checkpoint(g: &GpuContext, req: &SimRequest, n: u32, next: wgpu::Buffer) {
if let Ok(mut c) = g.particle_states.lock() {
let now = c.clock;
c.list.retain(|k| !(k.key == req.key && k.steps == req.steps));
c.list.push(Checkpoint { key: req.key, steps: req.steps, n, buf: next, used: now });
while c.list.len() > CHECKPOINTS {
let Some(i) = c.list.iter().enumerate().min_by_key(|(_, k)| k.used).map(|(i, _)| i) else { break };
c.list.remove(i);
}
}
}
pub(crate) type PipesCell = OnceLock<Pipes>;
pub(crate) type StatesCell = Mutex<Checkpoints>;