From 65e01c04df61433a6a9149c53e9a97e0ef3d8cf6 Mon Sep 17 00:00:00 2001 From: Haixuan Xavier Tao Date: Thu, 16 Jul 2026 15:21:37 +0200 Subject: [PATCH] narrow-phase: optional contact reduction (gpu_reduce_contacts) Training-grade, default OFF (pipeline.contact_reduction). A convex resting on a trimesh emits one manifold per overlapping triangle, all sharing one colliders key and one collider-A local frame; the mb contact-constraint kernels then pay 6 solver slots per point (4-8x flat-ground cost). The pass compacts each batch's contacts in place after pfm_pfm: first record of a pair copies verbatim (flat-ground = bit-identical, verified on the trainer's iter-0 reference), later records merge via the existing manifold_reduction selector (pool <=8 points, deepest manifold's normal, deepest+spread down to 4). threads(1) per batch, single writer. Documented approximations: one normal per merged manifold, greedy emission-order merging. Full-stack terrain @4096: 3.8 -> 3.3 s/iter. Co-Authored-By: Claude Fable 5 --- src_rbd/broad_phase/narrow_phase.rs | 19 +++++ src_rbd/pipeline/rbd_step.rs | 5 ++ src_rbd_shaders/broad_phase/narrow_phase.rs | 83 ++++++++++++++++++++ src_rbd_shaders/queries/polygonal_feature.rs | 5 ++ 4 files changed, 112 insertions(+) diff --git a/src_rbd/broad_phase/narrow_phase.rs b/src_rbd/broad_phase/narrow_phase.rs index b37cfb5..4f549c9 100644 --- a/src_rbd/broad_phase/narrow_phase.rs +++ b/src_rbd/broad_phase/narrow_phase.rs @@ -3,6 +3,8 @@ use crate::math::Pose; use crate::queries::GpuIndexedContact; use crate::shaders::PaddedVector; +#[cfg(feature = "dim3")] +use crate::shaders::broad_phase::GpuReduceContacts; use crate::shaders::broad_phase::{ CollisionPair, GpuInitPfmPfmDispatch, GpuNarrowPhaseInitContactsDispatch, GpuNarrowPhasePfmPfm, GpuNarrowPhaseShapeShape, GpuNarrowPhaseShapeShapeDeferred, GpuResetNarrowPhase, @@ -22,6 +24,8 @@ pub struct GpuNarrowPhase { /// `pfm_pairs` work-list. Split from `narrow_phase` to fit 8 storage buffers. narrow_phase_deferred: GpuNarrowPhaseShapeShapeDeferred, narrow_phase_pfm_pfm: GpuNarrowPhasePfmPfm, + #[cfg(feature = "dim3")] + reduce_contacts: GpuReduceContacts, init_pfm_pfm_indirect_args: GpuInitPfmPfmDispatch, init_contacts_indirect_args: GpuNarrowPhaseInitContactsDispatch, } @@ -48,6 +52,9 @@ impl GpuNarrowPhase { batch_indices: &Tensor, collider_parent: &Tensor, collider_materials: &Tensor, + // Optional: merge each collider pair's manifolds into one before the + // solvers see them. `false` skips the kernel entirely. + reduce_contacts: bool, ) -> Result<(), GpuBackendError> { let num_batches = contacts_len.len() as u32; self.reset_narrow_phase @@ -98,6 +105,18 @@ impl GpuNarrowPhase { collider_parent, collider_materials, )?; + #[cfg(feature = "dim3")] + if reduce_contacts { + self.reduce_contacts.call( + pass, + [1u32, num_batches, 1], + contacts, + contacts_len, + batch_indices, + )?; + } + #[cfg(not(feature = "dim3"))] + let _ = reduce_contacts; self.init_contacts_indirect_args .call(pass, 1u32, contacts_len, contacts_indirect)?; diff --git a/src_rbd/pipeline/rbd_step.rs b/src_rbd/pipeline/rbd_step.rs index 64817d2..c6f4162 100644 --- a/src_rbd/pipeline/rbd_step.rs +++ b/src_rbd/pipeline/rbd_step.rs @@ -32,6 +32,9 @@ pub struct RbdPipeline { coloring: GpuColoring, warmstart: GpuWarmstart, reduce: Reduce, + /// Optional (default `false`): merge each collider pair's manifolds + /// (e.g. per-triangle trimesh contacts) into one before the solvers. + pub contact_reduction: bool, } impl RbdPipeline { @@ -54,6 +57,7 @@ impl RbdPipeline { coloring: GpuColoring::from_backend(backend)?, warmstart: GpuWarmstart::from_backend(backend)?, reduce: Reduce::from_backend(backend)?, + contact_reduction: false, }) } @@ -197,6 +201,7 @@ impl RbdPipeline { &state.batch_indices, &state.collider_parent, &state.collider_materials, + self.contact_reduction, )?; drop(pass); diff --git a/src_rbd_shaders/broad_phase/narrow_phase.rs b/src_rbd_shaders/broad_phase/narrow_phase.rs index 20a4ead..f072dc4 100644 --- a/src_rbd_shaders/broad_phase/narrow_phase.rs +++ b/src_rbd_shaders/broad_phase/narrow_phase.rs @@ -6,6 +6,8 @@ use crate::queries::{ ColliderMaterial, ContactManifold, IndexedManifold, ball_ball, ball_convex, convex_ball, cuboid_cuboid, pfm_pfm, }; +#[cfg(feature = "dim3")] +use crate::queries::{ContactPoint, MAX_MANIFOLD_POINTS, manifold_reduction}; use crate::shapes::{ Capsule, Polyline, SHAPE_TYPE_BALL, SHAPE_TYPE_CAPSULE, SHAPE_TYPE_CONE, SHAPE_TYPE_CUBOID, SHAPE_TYPE_CYLINDER, SHAPE_TYPE_POLYLINE, SHAPE_TYPE_TRIMESH, Shape, TriMesh, @@ -70,6 +72,87 @@ pub fn gpu_narrow_phase_init_contacts_dispatch( *indirect_args.at_mut(2) = 1; } +/// Optional contact reduction: compacts each batch's contacts in place by +/// merging all manifolds of a collider pair (e.g. per-triangle trimesh +/// contacts, which share one `colliders` key and one collider-A local frame) +/// into a single `MAX_MANIFOLD_POINTS` manifold via `manifold_reduction`, +/// keeping the deeper manifold's normal. The first record of a pair is kept +/// verbatim, so single-manifold pairs are bit-identical to the unreduced +/// path. Approximations: one normal per merged manifold, greedy merging in +/// emission order. Grid `[1, num_batches, 1]`, serial per batch. +#[cfg(feature = "dim3")] +#[spirv_bindgen] +#[spirv(compute(threads(1)))] +pub fn gpu_reduce_contacts( + #[spirv(workgroup_id)] workgroup_id: UVec3, + #[spirv(storage_buffer, descriptor_set = 0, binding = 0)] contacts: &mut [IndexedManifold], + #[spirv(storage_buffer, descriptor_set = 0, binding = 1)] contacts_len: &mut [u32], + #[spirv(uniform, descriptor_set = 0, binding = 2)] batch_ids: &BatchIndices, +) { + let batch_id = workgroup_id.y; + let capacity = batch_ids.contacts_batch_capacity as usize; + let mut contacts = batch_ids.contact_batch_mut(batch_id, contacts); + let n = (contacts_len.read(batch_id as usize) as usize).min(capacity); + + let mut w = 0usize; // write cursor — always ≤ read cursor, in-place safe + for i in 0..n { + let im = contacts[i]; + let mut merged = false; + for j in 0..w { + let out = contacts[j]; + if out.colliders.x == im.colliders.x && out.colliders.y == im.colliders.y { + // Pool the two manifolds' points (same collider-A local frame). + let na = (out.contact.len as usize).min(MAX_MANIFOLD_POINTS); + let nb = (im.contact.len as usize).min(MAX_MANIFOLD_POINTS); + let mut cand = [ContactPoint::default(); 8]; + for k in 0..na { + cand.write(k, out.contact.points_a.read(k)); + } + for k in 0..nb { + cand.write(na + k, im.contact.points_a.read(k)); + } + // Normal of whichever manifold holds the deepest point. + let mut deep_out = out.contact.points_a.at(0).dist; + for k in 1..na { + let d = out.contact.points_a.at(k).dist; + if d < deep_out { + deep_out = d; + } + } + let mut deep_in = im.contact.points_a.at(0).dist; + for k in 1..nb { + let d = im.contact.points_a.at(k).dist; + if d < deep_in { + deep_in = d; + } + } + let normal = if deep_in < deep_out { + im.contact.normal_a + } else { + out.contact.normal_a + }; + let mut reduced = manifold_reduction(&cand, (na + nb) as u32, normal); + // `manifold_reduction` fills points/len only. + reduced.normal_a = normal; + let mut kept = out; + kept.contact = reduced; + contacts[j] = kept; + merged = true; + break; + } + } + if !merged { + contacts[w] = im; + w += 1; + } + } + // Compacted count; plain store, single writer per batch. (Loop shell per + // the `gpu_reset_narrow_phase` rustgpu-triviality workaround.) + for _ in 0..1 { + contacts_len.write(batch_id as usize, w as u32); + } +} + const PREDICTION: f32 = 2.0e-3; // TODO: make the prediction configurable. /// Narrow phase, pass 1 of 2: analytic shape-shape contacts for ball / cuboid diff --git a/src_rbd_shaders/queries/polygonal_feature.rs b/src_rbd_shaders/queries/polygonal_feature.rs index 29c8ddd..47395b2 100644 --- a/src_rbd_shaders/queries/polygonal_feature.rs +++ b/src_rbd_shaders/queries/polygonal_feature.rs @@ -288,6 +288,11 @@ mod dim2 { // 3D Implementation // ==================== +/// Re-export of the ≤8 → ≤4 keep-deepest-then-spread contact selector for the +/// optional contact-reduction pass (see `gpu_reduce_contacts`). +#[cfg(feature = "dim3")] +pub use dim3::manifold_reduction; + #[cfg(feature = "dim3")] mod dim3 { use super::*;