use super::pipeline::{create_fluid_pipelines, FluidPipelines};
use super::types::*;
use crate::gpu_types::Vertex;
use wgpu::util::DeviceExt;
mod geometry;
mod passes;
mod ssfr;
use geometry::alloc_sphere_verts;
use ssfr::create_ssfr_sized;
pub struct GpuFluidSystem {
pub num_particles: u32,
pub total_cells: u32,
pub particles_buffer: wgpu::Buffer,
pub params_buffer: wgpu::Buffer,
pub grid_buffer: wgpu::Buffer,
pub colliders_buffer: wgpu::Buffer,
pub sort_buffer: wgpu::Buffer,
pub sort_params_buffer: wgpu::Buffer,
pub pipelines: FluidPipelines,
pub mesh_vertices: wgpu::Buffer,
pub index_count: u32,
pub vertex_count: u32,
pub ssfr_particle_bg: wgpu::BindGroup,
pub ssfr_blur_x_bg: wgpu::BindGroup,
pub ssfr_blur_y_bg: wgpu::BindGroup,
pub ssfr_composite_bg: wgpu::BindGroup,
pub depth_texture_view: wgpu::TextureView,
pub raw_depth_texture: wgpu::Texture,
pub raw_depth_texture_view: wgpu::TextureView,
pub blur_texture_view: wgpu::TextureView,
pub thickness_texture_view: wgpu::TextureView,
pub opaque_bg_texture: wgpu::Texture,
pub opaque_bg_texture_view: wgpu::TextureView,
}
struct SsfrSized {
depth_texture_view: wgpu::TextureView,
raw_depth_texture: wgpu::Texture,
raw_depth_texture_view: wgpu::TextureView,
blur_texture_view: wgpu::TextureView,
thickness_texture_view: wgpu::TextureView,
opaque_bg_texture: wgpu::Texture,
opaque_bg_texture_view: wgpu::TextureView,
ssfr_particle_bg: wgpu::BindGroup,
ssfr_blur_x_bg: wgpu::BindGroup,
ssfr_blur_y_bg: wgpu::BindGroup,
ssfr_composite_bg: wgpu::BindGroup,
}
impl GpuFluidSystem {
pub fn new(
device: &wgpu::Device,
_queue: &wgpu::Queue,
num_particles: u32,
global_bind_group_layout: &wgpu::BindGroupLayout,
output_format: wgpu::TextureFormat,
width: u32,
height: u32,
) -> Self {
let n = num_particles as usize;
let spacing = 0.077_f32;
let nx = ((10.0 - 0.2) / spacing).floor() as usize; let nz = ((10.0 - 0.2) / spacing).floor() as usize;
let mut initial_particles = Vec::with_capacity(n);
for i in 0..n {
let xi = (i % nx) as f32;
let zi = ((i / nx) % nz) as f32;
let yi = (i / (nx * nz)) as f32;
let offset_x = -4.9; let offset_z = -4.9;
let x = offset_x + xi * spacing;
let y = 0.1 + yi * spacing; let z = offset_z + zi * spacing;
initial_particles.push(FluidParticle {
position: [x, y, z],
density: 1000.0,
velocity: [0.0, 0.0, 0.0],
lambda: 0.0,
predicted_position: [x, y, z],
phase: 0xFFFFFFFF, vorticity: [0.0, 0.0, 0.0],
_pad_vort: 0.0,
});
}
let particles_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("Fluid Particles Buffer"),
contents: bytemuck::cast_slice(&initial_particles),
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
});
let bounds_min = [-5.0, 0.0, -5.0];
let bounds_max = [5.0, 10.0, 5.0];
let cell_size = 0.1_f32;
let grid_size_x = f32::ceil((bounds_max[0] - bounds_min[0]) / cell_size) as u32 + 1;
let grid_size_y = f32::ceil((bounds_max[1] - bounds_min[1]) / cell_size) as u32 + 1;
let grid_size_z = f32::ceil((bounds_max[2] - bounds_min[2]) / cell_size) as u32 + 1;
let total_cells = grid_size_x * grid_size_y * grid_size_z;
let grid_initial = vec![0_u32; (total_cells * 2) as usize];
let grid_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("Fluid Grid Buffer"),
contents: bytemuck::cast_slice(&grid_initial),
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
});
let sort_capacity = num_particles.next_power_of_two() as usize;
let mut sort_initial = Vec::with_capacity(sort_capacity);
for i in 0..num_particles {
sort_initial.push(ParticleHash { hash: 0, index: i });
}
for _ in num_particles..sort_capacity as u32 {
sort_initial.push(ParticleHash {
hash: 0xFFFFFFFF,
index: 0,
});
}
let sort_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("Fluid Sort Buffer"),
contents: bytemuck::cast_slice(&sort_initial),
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
});
let num_elements = num_particles.next_power_of_two();
let mut sort_params_data = Vec::new();
let mut k = 2u32;
while k <= num_elements {
let mut j = k >> 1;
while j > 0 {
let params = SortParams {
j,
k,
_pad0: 0,
_pad1: 0,
};
sort_params_data.extend_from_slice(bytemuck::cast_slice(&[params]));
sort_params_data.extend_from_slice(&[0u8; 256 - 16]); j >>= 1;
}
k <<= 1;
}
let sort_params_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("Fluid Sort Params Buffer"),
contents: &sort_params_data,
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
});
let params = FluidParams {
dt: 0.016, gravity: 9.81,
rest_density: 1000.0,
gas_constant: 10000.0,
viscosity: 1.0,
mass: 0.457,
smoothing_radius: cell_size,
num_particles,
grid_size_x,
grid_size_y,
grid_size_z,
cell_size,
bounds_min,
bounds_padding1: 0.0,
bounds_max,
bounds_padding2: 0.0,
mouse_pos: [0.0; 3],
mouse_active: 0.0,
mouse_dir: [0.0; 3],
mouse_radius: 5.0,
num_colliders: 0,
cohesion: 0.008,
time: 0.0,
vorticity_strength: 0.35, surface_tension: 0.5, viscosity_laplacian: 0.005, xsph_factor: 0.05, solver_iterations: 6, };
let params_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("Fluid Params Buffer"),
contents: bytemuck::cast_slice(&[params]),
usage: wgpu::BufferUsages::UNIFORM
| wgpu::BufferUsages::COPY_DST
| wgpu::BufferUsages::COPY_SRC, });
let empty_colliders = vec![
FluidCollider {
position: [0.0; 3],
radius: 0.0,
velocity: [0.0; 3],
shape_type: 0,
half_extents: [0.0; 3],
_pad: 0.0,
};
MAX_FLUID_COLLIDERS
];
let colliders_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("Fluid Colliders Buffer"),
contents: bytemuck::cast_slice(&empty_colliders),
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
});
let pipelines = create_fluid_pipelines(
device,
global_bind_group_layout,
output_format,
¶ms_buffer,
&particles_buffer,
&grid_buffer,
&colliders_buffer,
&sort_buffer,
&sort_params_buffer,
);
let sphere_mesh = crate::asset::AssetManager::create_sphere(device, 0.25, 12, 12);
let mesh_vertices = wgpu::util::DeviceExt::create_buffer_init(
device,
&wgpu::util::BufferInitDescriptor {
label: Some("Fluid Sphere Verts"),
contents: bytemuck::cast_slice(&alloc_sphere_verts(0.25, 12, 12)),
usage: wgpu::BufferUsages::VERTEX,
},
);
let ssfr = create_ssfr_sized(
device,
&pipelines,
&particles_buffer,
output_format,
width,
height,
);
Self {
num_particles,
total_cells,
particles_buffer,
params_buffer,
grid_buffer,
colliders_buffer,
sort_buffer,
sort_params_buffer,
pipelines,
mesh_vertices,
index_count: 0,
vertex_count: sphere_mesh.vertex_count,
ssfr_particle_bg: ssfr.ssfr_particle_bg,
ssfr_blur_x_bg: ssfr.ssfr_blur_x_bg,
ssfr_blur_y_bg: ssfr.ssfr_blur_y_bg,
ssfr_composite_bg: ssfr.ssfr_composite_bg,
depth_texture_view: ssfr.depth_texture_view,
raw_depth_texture: ssfr.raw_depth_texture,
raw_depth_texture_view: ssfr.raw_depth_texture_view,
blur_texture_view: ssfr.blur_texture_view,
thickness_texture_view: ssfr.thickness_texture_view,
opaque_bg_texture: ssfr.opaque_bg_texture,
opaque_bg_texture_view: ssfr.opaque_bg_texture_view,
}
}
pub fn resize(
&mut self,
device: &wgpu::Device,
output_format: wgpu::TextureFormat,
width: u32,
height: u32,
) {
if width == 0 || height == 0 {
return;
}
let ssfr = create_ssfr_sized(
device,
&self.pipelines,
&self.particles_buffer,
output_format,
width,
height,
);
self.ssfr_particle_bg = ssfr.ssfr_particle_bg;
self.ssfr_blur_x_bg = ssfr.ssfr_blur_x_bg;
self.ssfr_blur_y_bg = ssfr.ssfr_blur_y_bg;
self.ssfr_composite_bg = ssfr.ssfr_composite_bg;
self.depth_texture_view = ssfr.depth_texture_view;
self.raw_depth_texture = ssfr.raw_depth_texture;
self.raw_depth_texture_view = ssfr.raw_depth_texture_view;
self.blur_texture_view = ssfr.blur_texture_view;
self.thickness_texture_view = ssfr.thickness_texture_view;
self.opaque_bg_texture = ssfr.opaque_bg_texture;
self.opaque_bg_texture_view = ssfr.opaque_bg_texture_view;
}
pub fn update_colliders_count(&self, queue: &wgpu::Queue, count: u32) {
queue.write_buffer(&self.params_buffer, 112, bytemuck::cast_slice(&[count]));
}
pub fn update_parameters(
&self,
queue: &wgpu::Queue,
mouse_pos: [f32; 3],
mouse_dir: [f32; 3],
mouse_active: bool,
colliders: &[FluidCollider],
time: f32,
active_particles: u32,
) {
let num_colliders = (colliders.len().min(MAX_FLUID_COLLIDERS)) as u32;
if num_colliders > 0 {
queue.write_buffer(
&self.colliders_buffer,
0,
bytemuck::cast_slice(&colliders[0..num_colliders as usize]),
);
}
#[repr(C)]
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
struct DynamicFluidParams {
mouse_pos: [f32; 3],
mouse_active: f32,
mouse_dir: [f32; 3],
mouse_radius: f32,
num_colliders: u32,
cohesion: f32,
time: f32,
vorticity_strength: f32,
surface_tension: f32,
viscosity_laplacian: f32,
xsph_factor: f32,
solver_iterations: u32,
}
let dyn_params = DynamicFluidParams {
mouse_pos,
mouse_active: if mouse_active { 1.0 } else { 0.0 },
mouse_dir,
mouse_radius: 10.0, num_colliders,
cohesion: 0.008, time,
vorticity_strength: 0.35, surface_tension: 0.5, viscosity_laplacian: 0.005, xsph_factor: 0.05, solver_iterations: 6, };
queue.write_buffer(&self.params_buffer, 80, bytemuck::cast_slice(&[dyn_params]));
queue.write_buffer(
&self.params_buffer,
28,
bytemuck::cast_slice(&[active_particles]),
);
}
}
#[cfg(test)]
mod gpu_dispatch_tests {
use super::*;
async fn setup_headless_gpu() -> Option<(wgpu::Device, wgpu::Queue)> {
crate::test_gpu::headless_device().await
}
async fn read_u32s(device: &wgpu::Device, queue: &wgpu::Queue, buffer: &wgpu::Buffer) -> Vec<u32> {
let size = buffer.size();
let staging = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("Test Staging Buffer"),
size,
usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let mut encoder =
device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None });
encoder.copy_buffer_to_buffer(buffer, 0, &staging, 0, size);
queue.submit(Some(encoder.finish()));
let slice = staging.slice(..);
let (tx, rx) = std::sync::mpsc::channel();
slice.map_async(wgpu::MapMode::Read, move |v| tx.send(v).unwrap());
let _ = device.poll(wgpu::PollType::Wait {
submission_index: None,
timeout: None,
});
rx.recv().unwrap().unwrap();
let data = slice.get_mapped_range();
let out = bytemuck::cast_slice(&data).to_vec();
drop(data);
staging.unmap();
out
}
#[test]
fn test_compute_pass_syncs_active_particle_count() {
let _gpu = crate::test_gpu::gpu_lock();
pollster::block_on(async {
let Some((device, queue)) = setup_headless_gpu().await else {
tracing::info!("Skipping GPU test: no wgpu adapter found");
return;
};
let global_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("test_fluid_global_layout"),
entries: &[wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
}],
});
let n = 1024u32;
let system = GpuFluidSystem::new(
&device,
&queue,
n,
&global_layout,
wgpu::TextureFormat::Rgba16Float,
256,
256,
);
let active = 300u32;
let mut encoder =
device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None });
system.compute_pass(&mut encoder, &queue, true, active);
queue.submit(Some(encoder.finish()));
let params_words = read_u32s(&device, &queue, &system.params_buffer).await;
assert_eq!(
params_words[7], active,
"compute_pass must sync params.num_particles (offset 28) to the active LOD count"
);
});
}
#[test]
fn test_resize_recreates_ssfr_textures() {
let _gpu = crate::test_gpu::gpu_lock();
pollster::block_on(async {
let Some((device, queue)) = setup_headless_gpu().await else {
tracing::info!("Skipping GPU test: no wgpu adapter found");
return;
};
let global_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("test_fluid_global_layout"),
entries: &[wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
}],
});
let fmt = wgpu::TextureFormat::Rgba16Float;
let mut system = GpuFluidSystem::new(&device, &queue, 256, &global_layout, fmt, 256, 256);
assert_eq!(
(system.raw_depth_texture.width(), system.raw_depth_texture.height()),
(256, 256)
);
assert_eq!(
(system.opaque_bg_texture.width(), system.opaque_bg_texture.height()),
(256, 256)
);
system.resize(&device, fmt, 512, 384);
assert_eq!(
(system.raw_depth_texture.width(), system.raw_depth_texture.height()),
(512, 384),
"resize must recreate raw_depth_texture at the new size"
);
assert_eq!(
(system.opaque_bg_texture.width(), system.opaque_bg_texture.height()),
(512, 384),
"resize must recreate opaque_bg_texture at the new size"
);
});
}
}