diff --git a/Cargo.toml b/Cargo.toml index 9276d02..0946f4d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,7 +26,7 @@ stensor = "0.4.2" [workspace.lints] rust.unexpected_cfgs = { level = "warn", check-cfg = [ - 'cfg(feature, values("dim2", "dim3"))', + 'cfg(feature, values("dim2", "dim3", "cpic", "node_particle_lists"))', ] } [patch.crates-io] diff --git a/crates/slosh2d/Cargo.toml b/crates/slosh2d/Cargo.toml index b73f94e..21c9b32 100644 --- a/crates/slosh2d/Cargo.toml +++ b/crates/slosh2d/Cargo.toml @@ -16,8 +16,10 @@ path = "../../src/lib.rs" required-features = ["dim2"] [features] -default = ["dim2"] +default = ["dim2", "cpic", "node_particle_lists"] dim2 = [] +cpic = [] +node_particle_lists = [] comptime = ["slosh_testbed2d/comptime", "stensor/comptime"] runtime = ["slosh_testbed2d/runtime", "stensor/runtime"] diff --git a/crates/slosh3d/Cargo.toml b/crates/slosh3d/Cargo.toml index b0c0b2d..251f757 100644 --- a/crates/slosh3d/Cargo.toml +++ b/crates/slosh3d/Cargo.toml @@ -16,8 +16,10 @@ path = "../../src/lib.rs" required-features = ["dim3"] [features] -default = ["dim3"] +default = ["dim3", "cpic", "node_particle_lists"] dim3 = [] +cpic = [] +node_particle_lists = [] comptime = ["stensor/comptime", "slosh_testbed3d/comptime"] runtime = ["stensor/runtime", "slosh_testbed3d/runtime"] diff --git a/shaders/slosh/grid/grid.slang b/shaders/slosh/grid/grid.slang index 5d9aa3f..6b771a4 100644 --- a/shaders/slosh/grid/grid.slang +++ b/shaders/slosh/grid/grid.slang @@ -2,6 +2,20 @@ module grid; import slosh.aliases; +// SLOSH_CPIC gates the CPIC parts of Node: the incompatible-momentum lane and the cdf collision +// field. register_shaders sets it from the cpic cargo feature so this layout matches the Rust +// GpuGridNode. Default on so a compile that never sets the macro still builds the full layout. +#ifndef SLOSH_CPIC +#define SLOSH_CPIC 1 +#endif + +// SLOSH_NODE_PARTICLE_LISTS gates the per-node particle linked lists (built in +// finalize_particles_sort, cleared in reset). Set from the node_particle_lists cargo feature; the +// bound buffers and the Rust GridArgs fields share it so everything stays in sync. +#ifndef SLOSH_NODE_PARTICLE_LISTS +#define SLOSH_NODE_PARTICLE_LISTS 1 +#endif + // TODO: a lot if what exposed from this module should be methods of Grid (or other structs) // rather than free function. (For now they have just been converted from WGSL to Slang.) @@ -192,11 +206,11 @@ void reset_hmap( ) { let id = invocation_id.x; if (id < grid[0].hmap_capacity) { + // Only `state` needs clearing: insertion CAS-probes on `state` and the winner writes + // `key`, and `find_block_header_id` reads `value` only after matching `state` (which is + // always written before any lookup). Skipping key/value saves 44 of the 48 bytes/entry + // on this clear, which sweeps the whole fixed-size hashmap every substep. hmap_entries[id].state = NONE; - // Resetting the following two isn’t necessary for correctness, - // but it makes debugging easier. - hmap_entries[id].key = BlockVirtualId(vector(0)); - hmap_entries[id].value = BlockHeaderId(0); } if (id == 0) { grid[0].num_active_blocks = 0u; @@ -309,12 +323,14 @@ public struct Node { /// (depending on the context). The fourth component contains the cell’s mass. // TODO: maybe we don’t really need to pack ourself. public vector momentum_velocity_mass; +#if SLOSH_CPIC // If a particle is incompatible with this node (as pepr CPIC’s concept of compatibility // based on affinities), it will contribute to this field instead of `momentum_velocity_mass`. // That way the P2G/G2P transfers on incompatible nodes still work properly and we don’t // loose the contributions of other compatible particles influencing this incompatible node too. public vector momentum_velocity_mass_incompatible; public NodeCdf cdf; +#endif } #if DIM == 2 @@ -442,8 +458,10 @@ func reset( uint3 invocation_id: SV_DispatchThreadID, StructuredBuffer grid, RWStructuredBuffer nodes, +#if SLOSH_NODE_PARTICLE_LISTS RWStructuredBuffer nodes_linked_lists, RWStructuredBuffer rigid_nodes_linked_lists, +#endif ) { // TODO: slang doesn’t have a way to retrieve the number of workgroups? // let num_threads = num_workgroups.x * GRID_WORKGROUP_SIZE * num_workgroups.y * num_workgroups.z; @@ -452,12 +470,16 @@ func reset( // for (var i = invocation_id.x; i < num_nodes; i += num_threads) { if (i < num_nodes) { nodes[i].momentum_velocity_mass = vector(0.0); +#if SLOSH_CPIC nodes[i].momentum_velocity_mass_incompatible = vector(0.0); nodes[i].cdf = NodeCdf(0.0, 0, NONE); +#endif +#if SLOSH_NODE_PARTICLE_LISTS nodes_linked_lists[i].head = NONE; nodes_linked_lists[i].len = 0u; rigid_nodes_linked_lists[i].head = NONE; rigid_nodes_linked_lists[i].len = 0u; +#endif } // } } diff --git a/shaders/slosh/grid/sort.slang b/shaders/slosh/grid/sort.slang index 37f9664..c25389d 100644 --- a/shaders/slosh/grid/sort.slang +++ b/shaders/slosh/grid/sort.slang @@ -3,6 +3,12 @@ module sort; import slosh.grid.grid; import slosh.solver.particle; +// SLOSH_NODE_PARTICLE_LISTS gates the per-node particle linked-list build (see grid.slang). +// Default on so a standalone compile keeps the full behavior. +#ifndef SLOSH_NODE_PARTICLE_LISTS +#define SLOSH_NODE_PARTICLE_LISTS 1 +#endif + // Returns the within-block sort bucket for a particle counted/inserted into its primary // block: one bucket per associated-cell slab along the slowest-varying node axis (y in 2D, // z in 3D). @@ -303,8 +309,10 @@ func finalize_particles_sort( StructuredBuffer hmap_entries, StructuredBuffer particles_pos, ConstantBuffer particles_len, +#if SLOSH_NODE_PARTICLE_LISTS RWStructuredBuffer nodes_linked_lists, RWStructuredBuffer particle_node_linked_lists, +#endif RWStructuredBuffer sorted_particle_ids, RWStructuredBuffer active_blocks, @@ -337,12 +345,14 @@ func finalize_particles_sort( } } +#if SLOSH_NODE_PARTICLE_LISTS // Setup the per-node particle linked-list (still consumed by the gather P2G and the // rigid/CDF paths). let node_global_id = node_id(block_header_id_to_physical_id(active_block_id_0), assoc); let prev_head = nodes_linked_lists[node_global_id.id].head.exchange(id); nodes_linked_lists[node_global_id.id].len.add(1u); particle_node_linked_lists[id] = prev_head; +#endif } } @@ -353,8 +363,10 @@ func sort_rigid_particles( StructuredBuffer grid, StructuredBuffer hmap_entries, StructuredBuffer rigid_particles_pos, +#if SLOSH_NODE_PARTICLE_LISTS RWStructuredBuffer rigid_nodes_linked_lists, RWStructuredBuffer rigid_particle_node_linked_lists, +#endif ) { let id = invocation_id.x; if (id < rigid_particles_pos.getCount()) { @@ -367,6 +379,7 @@ func sort_rigid_particles( // NOTE: if the rigid particle doesn’t map to any block, we can just ignore it // is it won’t affect the simulation. +#if SLOSH_NODE_PARTICLE_LISTS if (active_block_id.id != NONE) { // Setup the per-node rigid particle linked-list. let node_local_id = associated_cell_index_in_block_off_by_one(particle, cell_width); @@ -375,5 +388,6 @@ func sort_rigid_particles( rigid_nodes_linked_lists[node_global_id.id].len.add(1u); rigid_particle_node_linked_lists[id] = prev_head; } +#endif } } \ No newline at end of file diff --git a/shaders/slosh/solver/grid_update.slang b/shaders/slosh/solver/grid_update.slang index 23f3c7a..d818cb5 100644 --- a/shaders/slosh/solver/grid_update.slang +++ b/shaders/slosh/solver/grid_update.slang @@ -7,6 +7,12 @@ import slosh.solver.params; import slosh.collision.collide; import slosh.solver.boundary_condition; +// SLOSH_CPIC gates the incompatible-momentum lane on Node (see grid.slang). Default on so a +// standalone compile matches the crate's default layout. +#ifndef SLOSH_CPIC +#define SLOSH_CPIC 1 +#endif + #if DIM == 2 static const uint WORKGROUP_SIZE_X = 8; static const uint WORKGROUP_SIZE_Y = 8; @@ -73,10 +79,12 @@ func grid_update( * Step 1: momentum -> velocity update (gravity + clamping). */ let momentum_velocity_mass = nodes[global_id].momentum_velocity_mass; - let momentum_velocity_mass_incompatible = nodes[global_id].momentum_velocity_mass_incompatible; var velocity_mass = update_single_cell(sim_params, cell_width, cell_pos, momentum_velocity_mass); +#if SLOSH_CPIC + let momentum_velocity_mass_incompatible = nodes[global_id].momentum_velocity_mass_incompatible; let velocity_mass_incompatible = update_single_cell(sim_params, cell_width, cell_pos, momentum_velocity_mass_incompatible); nodes[global_id].momentum_velocity_mass_incompatible = velocity_mass_incompatible; +#endif /* * Step 2: rigid-body collision boundary condition. diff --git a/src/grid/grid.rs b/src/grid/grid.rs index f4250f5..58bdcce 100644 --- a/src/grid/grid.rs +++ b/src/grid/grid.rs @@ -34,7 +34,11 @@ struct GridArgs<'a, B: Backend> { n_block_groups: &'a GpuVector<[u32; 3], B>, n_g2p_p2g_groups: &'a GpuVector<[u32; 3], B>, nodes: &'a GpuVector, + // Per-node particle linked lists, bound only under the node_particle_lists feature (the + // shader params are #if SLOSH_NODE_PARTICLE_LISTS). + #[cfg(feature = "node_particle_lists")] nodes_linked_lists: &'a GpuVector<[u32; 2], B>, + #[cfg(feature = "node_particle_lists")] rigid_nodes_linked_lists: &'a GpuVector<[u32; 2], B>, scan_values: &'a GpuVector, // Snapshot of `num_active_blocks` after the primary-block touch pass (the base-block count), @@ -47,6 +51,7 @@ struct GridArgs<'a, B: Backend> { rigid_particles_pos: &'a GpuVector, rigid_particle_needs_block: &'a GpuVector, sorted_particle_ids: &'a GpuVector, + #[cfg(feature = "node_particle_lists")] particle_node_linked_lists: &'a GpuVector, } @@ -80,6 +85,7 @@ impl WgGrid { prefix_sum: &mut PrefixSumWorkspace, sort_module: &'a WgSort, prefix_sum_module: &'a WgPrefixSum, + reset_nodes: bool, ) -> Result<(), B::Error> { let args = GridArgs { grid: &grid.meta, @@ -87,7 +93,9 @@ impl WgGrid { n_block_groups: &grid.indirect_n_blocks_groups, n_g2p_p2g_groups: &grid.indirect_n_g2p_p2g_groups, nodes: &grid.nodes, + #[cfg(feature = "node_particle_lists")] nodes_linked_lists: &grid.nodes_linked_lists, + #[cfg(feature = "node_particle_lists")] rigid_nodes_linked_lists: &grid.rigid_nodes_linked_lists, scan_values: &grid.scan_values, num_base_blocks: &grid.active_blocks_snapshot, @@ -97,6 +105,7 @@ impl WgGrid { rigid_particles_pos: &rigid_particles.sample_points, rigid_particle_needs_block: &rigid_particles.rigid_particle_needs_block, sorted_particle_ids: particles.sorted_ids(), + #[cfg(feature = "node_particle_lists")] particle_node_linked_lists: particles.node_linked_lists(), }; @@ -202,14 +211,22 @@ impl WgGrid { .copy_scan_values_to_first_particles_and_prepare_for_finalize .launch_indirect(backend, pass, &args, grid.indirect_n_blocks_groups.buffer())?; - // Reset here so the linked list heads get reset before `finalize_particles_sort` which - // also setups the per-node linked list. - self.reset.launch_indirect( - backend, - pass, - &args, - grid.indirect_n_g2p_p2g_groups.buffer(), - )?; + // `reset` zeroes the node momentum lane, but it's also the only thing that initializes + // the per-node data the features depend on: the cpic incompatible/cdf lanes that + // grid_update reads, and the linked-list heads/len that finalize_particles_sort builds + // onto. Those buffers start uninitialized, so we can only honor `reset_nodes: false` when + // both features are off and the momentum lane (overwritten by every P2G) is all that's + // left. It has to run before finalize_particles_sort, which populates the linked list. + let must_reset = + reset_nodes || cfg!(feature = "cpic") || cfg!(feature = "node_particle_lists"); + if must_reset { + self.reset.launch_indirect( + backend, + pass, + &args, + grid.indirect_n_g2p_p2g_groups.buffer(), + )?; + } sort_module.finalize_particles_sort.launch_capped( backend, pass, @@ -241,7 +258,12 @@ pub struct GpuGridMetadata { #[repr(C)] pub struct GpuGridNode { momentum_velocity_mass: glam::Vec4, + // Gated by the cpic feature (default on). Turning cpic off drops the node from 48 to 16 + // bytes, cutting grid traffic ~3x, and gives up collision-detection-field support. Keep in + // lockstep with Node in grid.slang, which SLOSH_CPIC gates the same way. + #[cfg(feature = "cpic")] momentum_velocity_mass_incompatible: glam::Vec4, + #[cfg(feature = "cpic")] cdf: GpuGridNodeCdf, } @@ -343,6 +365,7 @@ pub struct GpuNodeCollision { /// Collision detection field data for a grid node. /// /// Stores signed distance and affinity information for MPM-rigid body coupling. +#[cfg(feature = "cpic")] #[derive(Copy, Clone, PartialEq, Default, Debug, ShaderType)] #[repr(C)] pub struct GpuGridNodeCdf { @@ -386,9 +409,12 @@ pub struct GpuGrid { pub active_blocks_snapshot: GpuVector, /// Workspace for prefix sum operations. pub scan_values: GpuVector, - /// Per-node linked lists for MPM particles. + /// Per-node linked lists for MPM particles. Only built/reset under the node_particle_lists + /// feature, but always allocated: the P2G launchers still bind this buffer (the gather P2G + /// reads it; the scatter-style P2G binds it without reading). Gating the allocation means + /// dropping that unused scatter-style binding first. pub nodes_linked_lists: GpuVector<[u32; 2], B>, - /// Per-node linked lists for rigid body particles. + /// Per-node linked lists for rigid body particles. See `nodes_linked_lists`. pub rigid_nodes_linked_lists: GpuVector<[u32; 2], B>, /// Indirect dispatch arguments for block-parallel kernels. pub indirect_n_blocks_groups: Arc>, diff --git a/src/grid/sort.rs b/src/grid/sort.rs index 1b3329b..fe20be4 100644 --- a/src/grid/sort.rs +++ b/src/grid/sort.rs @@ -38,8 +38,10 @@ pub struct WgSort { struct SortArgs<'a, B: Backend> { grid: &'a GpuScalar, hmap_entries: &'a GpuScalar, + #[cfg(feature = "node_particle_lists")] rigid_nodes_linked_lists: &'a GpuScalar<[u32; 2], B>, rigid_particles_pos: &'a GpuScalar, + #[cfg(feature = "node_particle_lists")] rigid_particle_node_linked_lists: &'a GpuScalar, } @@ -64,8 +66,10 @@ impl WgSort { let args = SortArgs { grid: &grid.meta, hmap_entries: &grid.hmap_entries, + #[cfg(feature = "node_particle_lists")] rigid_nodes_linked_lists: &grid.rigid_nodes_linked_lists, rigid_particles_pos: &rigid_particles.sample_points, + #[cfg(feature = "node_particle_lists")] rigid_particle_node_linked_lists: &rigid_particles.node_linked_lists, }; diff --git a/src/lib.rs b/src/lib.rs index ee509d6..e978134 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -89,6 +89,18 @@ pub const SLANG_SRC_DIR: include_dir::Dir<'_> = pub fn register_shaders(compiler: &mut SlangCompiler) { stensor::register_shaders(compiler); compiler.add_dir(SLANG_SRC_DIR.clone()); + // Mirror the cpic and node_particle_lists cargo features into shader macros so the shader + // Node layout and sort kernels match GpuGridNode and GridArgs on the Rust side. slosh sets + // these itself (unlike DIM, which the consumer picks) since they're crate-layout invariants. + compiler.set_global_macro("SLOSH_CPIC", if cfg!(feature = "cpic") { 1 } else { 0 }); + compiler.set_global_macro( + "SLOSH_NODE_PARTICLE_LISTS", + if cfg!(feature = "node_particle_lists") { + 1 + } else { + 0 + }, + ); } /// Mathematical types and utilities for physics simulation. diff --git a/src/pipeline.rs b/src/pipeline.rs index aaebd47..b71063b 100644 --- a/src/pipeline.rs +++ b/src/pipeline.rs @@ -12,9 +12,12 @@ use crate::rbd::dynamics::body::{BodyCoupling, BodyCouplingEntry}; use crate::solver::{ GpuBoundaryCondition, GpuImpulses, GpuMaterials, GpuParticleModelData, GpuParticles, GpuRigidParticles, GpuSimulationParams, GpuTimestepBounds, Particle, SimulationParams, WgG2P, - WgG2PCdf, WgGridUpdate, WgGridUpdateCdf, WgP2G, WgP2GCdf, WgP2GScatterStyle, WgParticleUpdate, - WgRigidImpulses, WgRigidParticleUpdate, WgTimestepBounds, + WgGridUpdate, WgP2G, WgP2GScatterStyle, WgParticleUpdate, WgRigidImpulses, + WgRigidParticleUpdate, WgTimestepBounds, }; +// The CDF kernel wrappers read the gated `Node.cdf`, so they only exist under the `cpic` feature. +#[cfg(feature = "cpic")] +use crate::solver::{WgG2PCdf, WgGridUpdateCdf, WgP2GCdf}; use rapier::dynamics::RigidBodySet; use rapier::geometry::{ColliderHandle, ColliderSet}; use slang_hal::backend::{Backend, Encoder, GpuTimestamps}; @@ -23,6 +26,72 @@ use std::any::Any; use std::marker::PhantomData; use stensor::tensor::{GpuScalar, GpuTensor, GpuVector}; +/// Selects which optional kernel families `MpmPipeline` compiles. +/// +/// A pipeline that never dispatches some kernels (say hooks replace the built-in P2G/G2P and the +/// CDF/rigid paths go unused) can skip compiling them. Skipping is mandatory once the `Node` +/// struct is slimmed: the CDF kernels read `Node.cdf`, so they cannot be compiled after `cpic` +/// gates that field out. +/// +/// The gather P2G and the CDF/rigid paths read the per-node linked lists, which only exist with +/// the `node_particle_lists` feature (on by default). Keep it enabled if you use +/// `builtin_transfers` (gather), `cdf`, or `rigid_particles`. +#[derive(Copy, Clone, Debug)] +pub struct MpmPipelineKernels { + /// Built-in P2G/G2P transfer kernels (gather + scatter). Skip only if hooks handle + /// `run_p2g`/`run_g2p` on every step. + pub builtin_transfers: bool, + /// CDF (rigid-body collision field) kernels. Read `Node.cdf`, so compiled only under the + /// `cpic` feature. Compile-only for now: `launch_step` doesn't dispatch the CDF passes yet. + pub cdf: bool, + /// Rigid-particle update kernel. Compile-only for now: `launch_step` doesn't dispatch the + /// rigid-particle pass yet. + pub rigid_particles: bool, + /// Run the per-node `reset` pass during the sort. Its only unconditional job is zeroing + /// `Node.momentum_velocity_mass`, which every P2G variant overwrites on every active node + /// anyway, so set `false` to skip it when the built-in P2G (or a `run_p2g` hook) is known to + /// write every active node. + /// + /// Only honored with both `cpic` and `node_particle_lists` off. With either on, `reset` also + /// does init those features rely on (the cpic incompatible/cdf lanes, the linked-list + /// heads/len), so `launch_sort` forces it regardless of this flag. + pub node_reset: bool, + /// Built-in grid_update pass (momentum to velocity, gravity, velocity clamp, rigid-collision + /// boundary condition). Disable only when a `run_p2g` hook folds this into its own kernel; + /// the grid must hold velocities by the time G2P runs. + pub grid_update: bool, + /// Built-in particles_update pass (advection, deformation-gradient update, constitutive + /// model, APIC affine). Disable only when a `run_g2p` hook folds this into its own kernel; + /// particle state must be fully advanced before `after_particles_update` hooks run. + pub particles_update: bool, +} + +impl Default for MpmPipelineKernels { + fn default() -> Self { + Self { + builtin_transfers: true, + cdf: true, + rigid_particles: true, + node_reset: true, + grid_update: true, + particles_update: true, + } + } +} + +impl MpmPipelineKernels { + /// Minimal set for pipelines whose hooks replace the built-in transfers and that don't use + /// the CDF or rigid-particle paths. + pub fn hooks_only() -> Self { + Self { + builtin_transfers: false, + cdf: false, + rigid_particles: false, + ..Self::default() + } + } +} + /// GPU compute pipeline for Material Point Method simulation. /// /// This struct holds all the compiled compute shaders needed to execute a complete @@ -45,23 +114,31 @@ pub struct MpmPipeline { sort: WgSort, // Kept for the alternative/CDF code paths that are currently commented out in `step`. #[allow(dead_code)] - p2g: WgP2G, - p2g_scatter_style: WgP2GScatterStyle, + p2g: Option>, + p2g_scatter_style: Option>, + // The CDF kernels read `Node.cdf`, so gate them behind `cpic`: a build without the feature + // (slim 16 B node) must not construct a kernel referencing the removed field. + #[cfg(feature = "cpic")] #[allow(dead_code)] - p2g_cdf: WgP2GCdf, + p2g_cdf: Option>, + #[cfg(feature = "cpic")] #[allow(dead_code)] - grid_update_cdf: WgGridUpdateCdf, - grid_update: WgGridUpdate, - particles_update: WgParticleUpdate, - g2p: WgG2P, + grid_update_cdf: Option>, + grid_update: Option>, + particles_update: Option>, + g2p: Option>, + #[cfg(feature = "cpic")] #[allow(dead_code)] - g2p_cdf: WgG2PCdf, + g2p_cdf: Option>, #[allow(dead_code)] - rigid_particles_update: WgRigidParticleUpdate, + rigid_particles_update: Option>, /// Maximum timestep bound calculation. pub timestep_bounds: WgTimestepBounds, /// Rigid body impulse computation kernel (publicly accessible for external use). pub impulses: WgRigidImpulses, + /// Kernel families this pipeline was built with; also gates which built-in dispatches + /// `launch_step` runs. + kernels: MpmPipelineKernels, _phantom: PhantomData, } @@ -377,26 +454,71 @@ impl MpmPipeline { /// /// A ready-to-use MPM pipeline, or an error if shader compilation fails. pub fn new(backend: &B, compiler: &SlangCompiler) -> Result { + Self::new_with_kernels(backend, compiler, MpmPipelineKernels::default()) + } + + /// Like [`Self::new`], but only compiles the kernel families selected by `kernels`. Any + /// skipped kernel must never be dispatched. + pub fn new_with_kernels( + backend: &B, + compiler: &SlangCompiler, + kernels: MpmPipelineKernels, + ) -> Result { Ok(Self { grid: WgGrid::from_backend(backend, compiler)?, prefix_sum: WgPrefixSum::from_backend(backend, compiler)?, sort: WgSort::from_backend(backend, compiler)?, - p2g: WgP2G::from_backend(backend, compiler)?, - p2g_scatter_style: WgP2GScatterStyle::from_backend(backend, compiler)?, - p2g_cdf: WgP2GCdf::from_backend(backend, compiler)?, - grid_update: WgGridUpdate::from_backend(backend, compiler)?, - grid_update_cdf: WgGridUpdateCdf::from_backend(backend, compiler)?, + p2g: kernels + .builtin_transfers + .then(|| WgP2G::from_backend(backend, compiler)) + .transpose()?, + p2g_scatter_style: kernels + .builtin_transfers + .then(|| WgP2GScatterStyle::from_backend(backend, compiler)) + .transpose()?, + #[cfg(feature = "cpic")] + p2g_cdf: kernels + .cdf + .then(|| WgP2GCdf::from_backend(backend, compiler)) + .transpose()?, + grid_update: kernels + .grid_update + .then(|| WgGridUpdate::from_backend(backend, compiler)) + .transpose()?, + #[cfg(feature = "cpic")] + grid_update_cdf: kernels + .cdf + .then(|| WgGridUpdateCdf::from_backend(backend, compiler)) + .transpose()?, #[cfg(feature = "comptime")] - particles_update: WgParticleUpdate::from_backend(backend, compiler)?, + particles_update: kernels + .particles_update + .then(|| WgParticleUpdate::from_backend(backend, compiler)) + .transpose()?, #[cfg(feature = "runtime")] - particles_update: WgParticleUpdate::with_specializations( - backend, - compiler, - &GpuModel::specialization_modules(), - )?, - rigid_particles_update: WgRigidParticleUpdate::from_backend(backend, compiler)?, - g2p: WgG2P::from_backend(backend, compiler)?, - g2p_cdf: WgG2PCdf::from_backend(backend, compiler)?, + particles_update: kernels + .particles_update + .then(|| { + WgParticleUpdate::with_specializations( + backend, + compiler, + &GpuModel::specialization_modules(), + ) + }) + .transpose()?, + rigid_particles_update: kernels + .rigid_particles + .then(|| WgRigidParticleUpdate::from_backend(backend, compiler)) + .transpose()?, + g2p: kernels + .builtin_transfers + .then(|| WgG2P::from_backend(backend, compiler)) + .transpose()?, + #[cfg(feature = "cpic")] + g2p_cdf: kernels + .cdf + .then(|| WgG2PCdf::from_backend(backend, compiler)) + .transpose()?, impulses: WgRigidImpulses::from_backend(backend, compiler)?, #[cfg(feature = "comptime")] timestep_bounds: WgTimestepBounds::from_backend(backend, compiler)?, @@ -406,6 +528,7 @@ impl MpmPipeline { compiler, &GpuModel::specialization_modules(), )?, + kernels, _phantom: PhantomData, }) } @@ -473,6 +596,7 @@ impl MpmPipeline { &mut data.prefix_sum, &self.sort, &self.prefix_sum, + self.kernels.node_reset, )?; // self.sort.launch_sort_rigid_particles( // backend, @@ -490,6 +614,9 @@ impl MpmPipeline { timestamps.as_deref_mut(), )?; + // CDF passes, not yet wired. The cdf kernels are now `#[cfg(feature = "cpic")] + // Option<..>`, so re-enabling this means gating the block on `cpic`, unwrapping with + // `.as_ref().expect(..)`, and setting `MpmPipelineKernels::cdf`. // { // let mut pass = encoder.begin_pass("grid_update_cdf", timestamps.as_deref_mut()); // self.grid_update_cdf @@ -528,15 +655,20 @@ impl MpmPipeline { timestamps.as_deref_mut(), )? { let mut pass = encoder.begin_pass("p2g", timestamps.as_deref_mut()); - self.p2g_scatter_style.launch( - backend, - &mut pass, - &data.grid, - &data.particles, - &data.impulses, - &data.bodies, - &data.body_materials, - )?; + self.p2g_scatter_style + .as_ref() + .expect( + "pipeline built without builtin transfer kernels; a hook must handle run_p2g", + ) + .launch( + backend, + &mut pass, + &data.grid, + &data.particles, + &data.impulses, + &data.bodies, + &data.body_materials, + )?; } hooks.after_p2g( @@ -547,9 +679,9 @@ impl MpmPipeline { timestamps.as_deref_mut(), )?; - { + if let Some(grid_update) = &self.grid_update { let mut pass = encoder.begin_pass("grid_update", timestamps.as_deref_mut()); - self.grid_update.launch( + grid_update.launch( backend, &mut pass, &data.sim_params, @@ -577,15 +709,20 @@ impl MpmPipeline { timestamps.as_deref_mut(), )? { let mut pass = encoder.begin_pass("g2p", timestamps.as_deref_mut()); - self.g2p.launch( - backend, - &mut pass, - &data.sim_params, - &data.grid, - &data.particles, - &data.bodies, - &data.body_materials, - )?; + self.g2p + .as_ref() + .expect( + "pipeline built without builtin transfer kernels; a hook must handle run_g2p", + ) + .launch( + backend, + &mut pass, + &data.sim_params, + &data.grid, + &data.particles, + &data.bodies, + &data.body_materials, + )?; } hooks.after_g2p( @@ -596,9 +733,9 @@ impl MpmPipeline { timestamps.as_deref_mut(), )?; - { + if let Some(particles_update) = &self.particles_update { let mut pass = encoder.begin_pass("particles_update", timestamps.as_deref_mut()); - self.particles_update.launch( + particles_update.launch( backend, &mut pass, &data.sim_params, diff --git a/src/solver/mod.rs b/src/solver/mod.rs index 41e99ef..c275352 100644 --- a/src/solver/mod.rs +++ b/src/solver/mod.rs @@ -34,8 +34,10 @@ //! - [`SimulationParams`]: Global parameters (gravity, timestep) pub use g2p::WgG2P; +#[cfg(feature = "cpic")] pub use g2p_cdf::WgG2PCdf; pub use p2g::{WgP2G, WgP2GScatterStyle}; +#[cfg(feature = "cpic")] pub use p2g_cdf::WgP2GCdf; pub use params::{GpuSimulationParams, SimulationParams}; pub use particle::*; @@ -43,6 +45,7 @@ pub use particle_model::*; // pub use particle_update::WgParticleUpdate; pub use boundary_condition::{GpuBoundaryCondition, GpuMaterials}; pub use grid_update::WgGridUpdate; +#[cfg(feature = "cpic")] pub use grid_update_cdf::WgGridUpdateCdf; pub use particle_update::WgParticleUpdate; pub use rigid_impulses::{GpuImpulses, RigidImpulse, WgRigidImpulses}; @@ -51,8 +54,10 @@ pub use timestep_bound::{GpuTimestepBounds, WgTimestepBounds}; mod boundary_condition; mod g2p; +#[cfg(feature = "cpic")] mod g2p_cdf; mod p2g; +#[cfg(feature = "cpic")] mod p2g_cdf; mod params; mod particle_update; @@ -60,6 +65,7 @@ mod rigid_impulses; mod rigid_particle_update; mod grid_update; +#[cfg(feature = "cpic")] mod grid_update_cdf; mod particle; mod particle_model; diff --git a/src_testbed/lib.rs b/src_testbed/lib.rs index 6a16a89..619404d 100644 --- a/src_testbed/lib.rs +++ b/src_testbed/lib.rs @@ -34,7 +34,7 @@ use slang_hal::re_exports::include_dir; use slang_hal::BufferUsages; use slang_hal::SlangCompiler; use slosh::math::Vector; -use slosh::pipeline::{MpmPipeline, MpmPipelineHooks}; +use slosh::pipeline::{MpmPipeline, MpmPipelineHooks, MpmPipelineKernels}; use slosh::rapier::geometry::Shape; use slosh::rapier::geometry::ShapeType; use slosh::rapier::prelude::ColliderHandle; @@ -46,6 +46,32 @@ use wgpu::Limits; type SceneBuilders = Vec<(String, SceneBuildFn)>; type SceneBuildFn = fn(&WebGpu, &mut AppState) -> PhysicsContext; +/// GPU-construction options for the testbed. +pub struct TestbedConfig { + /// Kernel families compiled into the `MpmPipeline`. Must match what the hooks expect: + /// hooks that *replace* built-in passes (e.g. fuse the grid update into P2G or the + /// particle update into G2P) require those passes disabled, or they run a second time. + pub kernels: MpmPipelineKernels, + /// Device limits requested from wgpu. Bump e.g. `max_storage_buffers_per_shader_stage` + /// when hook kernels bind more buffers than the built-in passes need. + pub limits: Limits, +} + +impl Default for TestbedConfig { + fn default() -> Self { + Self { + kernels: MpmPipelineKernels::default(), + limits: Limits { + max_storage_buffers_per_shader_stage: 13, + max_compute_workgroup_storage_size: 32768, // Why do we need this if wgsparkl didn’t? + max_buffer_size: 4_000_000_000, + max_storage_buffer_binding_size: 4_000_000_000, + ..Limits::default() + }, + } + } +} + #[cfg(feature = "dim2")] type RenderNode = PlanarSceneNode; #[cfg(feature = "dim3")] @@ -79,15 +105,9 @@ impl Stage { mut compiler: SlangCompiler, hooks: impl FnOnce(&WebGpu, &SlangCompiler) -> Box>, builders: SceneBuilders, + config: TestbedConfig, ) -> Stage { - let limits = Limits { - max_storage_buffers_per_shader_stage: 13, - max_compute_workgroup_storage_size: 32768, // Why do we need this if wgsparkl didn’t? - max_buffer_size: 4_000_000_000, - max_storage_buffer_binding_size: 4_000_000_000, - ..Limits::default() - }; - let mut gpu = WebGpu::new(wgpu::Features::TIMESTAMP_QUERY, limits) + let mut gpu = WebGpu::new(wgpu::Features::TIMESTAMP_QUERY, config.limits) .await .unwrap(); // TODO: this is a terrible, horrible, hack, to work around the fact that slang isn’t giving us access to @@ -109,7 +129,7 @@ impl Stage { compiler.set_global_macro("DIM", slosh::math::DIM); } - let mpm_pipeline = MpmPipeline::new(&gpu, &compiler).unwrap(); + let mpm_pipeline = MpmPipeline::new_with_kernels(&gpu, &compiler, config.kernels).unwrap(); let mut app_state = AppState { pipeline: mpm_pipeline, run_state: RunState::Paused, @@ -350,10 +370,33 @@ pub async fn run_with_hooks( hooks: impl FnOnce(&WebGpu, &SlangCompiler) -> Box>, scene_builders: SceneBuilders, #[cfg(feature = "dim3")] up_axis: Vector, +) { + run_with_hooks_and_config( + compiler, + hooks, + TestbedConfig::default(), + scene_builders, + #[cfg(feature = "dim3")] + up_axis, + ) + .await +} + +/// Like [`run_with_hooks`], but with explicit GPU-construction options (kernel set, device +/// limits). Required when the hooks *replace* built-in passes (e.g. fuse the grid update +/// into P2G or the particle update into G2P): the default kernel set would run those passes +/// a second time. +pub async fn run_with_hooks_and_config( + compiler: SlangCompiler, + hooks: impl FnOnce(&WebGpu, &SlangCompiler) -> Box>, + config: TestbedConfig, + scene_builders: SceneBuilders, + #[cfg(feature = "dim3")] up_axis: Vector, ) { run_with_hooks_and_ui( compiler, hooks, + config, scene_builders, |_, _, _, _| None, #[cfg(feature = "dim3")] @@ -365,6 +408,7 @@ pub async fn run_with_hooks( pub async fn run_with_hooks_and_ui( compiler: SlangCompiler, hooks: impl FnOnce(&WebGpu, &SlangCompiler) -> Box>, + config: TestbedConfig, scene_builders: SceneBuilders, mut extra_ui: impl FnMut( &egui::Context, @@ -375,7 +419,7 @@ pub async fn run_with_hooks_and_ui( #[cfg(feature = "dim3")] up_axis: Vector, ) { let mut colliders_gfx = HashMap::new(); - let mut stage = Stage::new(compiler, hooks, scene_builders).await; + let mut stage = Stage::new(compiler, hooks, scene_builders, config).await; let mut window = Window::new("slosh - 3D testbed"); render_colliders(&mut window, &stage.physics, &mut colliders_gfx); @@ -604,9 +648,28 @@ pub async fn run_headless_with_hooks( compiler: SlangCompiler, hooks: impl FnOnce(&WebGpu, &SlangCompiler) -> Box>, scene_builders: SceneBuilders, + step_callback: impl FnMut(&PhysicsContext, &SimulationStepResult) -> bool, +) { + run_headless_with_hooks_and_config( + compiler, + hooks, + TestbedConfig::default(), + scene_builders, + step_callback, + ) + .await +} + +/// Like [`run_headless_with_hooks`], but with explicit GPU-construction options. +/// See [`run_with_hooks_and_config`]. +pub async fn run_headless_with_hooks_and_config( + compiler: SlangCompiler, + hooks: impl FnOnce(&WebGpu, &SlangCompiler) -> Box>, + config: TestbedConfig, + scene_builders: SceneBuilders, mut step_callback: impl FnMut(&PhysicsContext, &SimulationStepResult) -> bool, ) { - let mut stage = Stage::new(compiler, hooks, scene_builders).await; + let mut stage = Stage::new(compiler, hooks, scene_builders, config).await; stage.app_state.run_state = RunState::Running; loop {