From 9d55895e12d9548759b1ad7fe15577707c925284 Mon Sep 17 00:00:00 2001 From: Haixuan Xavier Tao Date: Mon, 27 Jul 2026 18:11:53 +0200 Subject: [PATCH 1/2] Make the narrow-phase contact prediction distance configurable MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The narrow-phase used a hardcoded 2mm PREDICTION constant (its TODO asked for this). RbdSimParams already carries normalized_prediction_distance; plumb prediction_distance() into the narrow-phase kernels as a scalar uniform. Default behavior unchanged. A wider margin matters for articulated statics: with the solver holding equilibrium penetration near zero, a 2mm window drops a box foot\x27s far-edge corners at ~0.5 deg of tilt — the manifold collapses to one edge (zero pitch moment capacity) and the foot rocks. PhysX ships ~2cm contactOffset for the same reason. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01B7NC7U2wDx2tSmT9mkeF9h --- src_rbd/broad_phase/narrow_phase.rs | 4 +++ src_rbd/pipeline/insertion_removal.rs | 7 +++++ src_rbd/pipeline/rbd_state.rs | 3 ++ src_rbd/pipeline/rbd_state_from_rapier.rs | 7 +++++ src_rbd/pipeline/rbd_step.rs | 1 + src_rbd_shaders/broad_phase/narrow_phase.rs | 31 +++++++++++++-------- 6 files changed, 42 insertions(+), 11 deletions(-) diff --git a/src_rbd/broad_phase/narrow_phase.rs b/src_rbd/broad_phase/narrow_phase.rs index b37cfb5..bf38e0b 100644 --- a/src_rbd/broad_phase/narrow_phase.rs +++ b/src_rbd/broad_phase/narrow_phase.rs @@ -48,6 +48,7 @@ impl GpuNarrowPhase { batch_indices: &Tensor, collider_parent: &Tensor, collider_materials: &Tensor, + prediction: &Tensor, ) -> Result<(), GpuBackendError> { let num_batches = contacts_len.len() as u32; self.reset_narrow_phase @@ -65,6 +66,7 @@ impl GpuNarrowPhase { batch_indices, collider_parent, collider_materials, + prediction, )?; // Pass 2: defer the complex shape pairs into `pfm_pairs` (kept as a @@ -79,6 +81,7 @@ impl GpuNarrowPhase { pfm_pairs, pfm_pairs_len, batch_indices, + prediction, vertices, indices, )?; @@ -97,6 +100,7 @@ impl GpuNarrowPhase { indices, collider_parent, collider_materials, + prediction, )?; self.init_contacts_indirect_args .call(pass, 1u32, contacts_len, contacts_indirect)?; diff --git a/src_rbd/pipeline/insertion_removal.rs b/src_rbd/pipeline/insertion_removal.rs index b5e1332..c43a095 100644 --- a/src_rbd/pipeline/insertion_removal.rs +++ b/src_rbd/pipeline/insertion_removal.rs @@ -144,6 +144,12 @@ impl RbdState { BufferUsages::STORAGE | BufferUsages::UNIFORM, ) .unwrap(); + let prediction = Tensor::scalar( + backend, + all_sim_params[0].prediction_distance(), + BufferUsages::UNIFORM | BufferUsages::COPY_DST, + ) + .unwrap(); // Two-element readback: the (max) collision-pair count and the uncolored count. let resize_readback = GpuReadback::new(backend, 2).unwrap(); let collision_pairs_indirect = @@ -250,6 +256,7 @@ impl RbdState { collision_pairs_len, collision_pairs_len_max, num_batches_uniform, + prediction, resize_readback, collision_pairs_indirect, contacts_per_batch_cpu, diff --git a/src_rbd/pipeline/rbd_state.rs b/src_rbd/pipeline/rbd_state.rs index 73e0c92..b9554e3 100644 --- a/src_rbd/pipeline/rbd_state.rs +++ b/src_rbd/pipeline/rbd_state.rs @@ -160,6 +160,9 @@ pub struct RbdState { /// Single-element scratch holding the max of `collision_pairs_len` across all /// batches, computed on the GPU (only used when `num_batches > 1`). pub(super) collision_pairs_len_max: Tensor, + /// Contact prediction distance (`RbdSimParams::prediction_distance`), + /// consumed by the narrow-phase kernels. + pub(super) prediction: Tensor, /// `num_batches` as a uniform, the scan length for the max reduction. pub(super) num_batches_uniform: Tensor, /// Non-blocking readback of `[max collision_pairs_len, uncolored]` used by diff --git a/src_rbd/pipeline/rbd_state_from_rapier.rs b/src_rbd/pipeline/rbd_state_from_rapier.rs index 92bdd1e..28679de 100644 --- a/src_rbd/pipeline/rbd_state_from_rapier.rs +++ b/src_rbd/pipeline/rbd_state_from_rapier.rs @@ -562,6 +562,12 @@ impl RbdState { BufferUsages::STORAGE | BufferUsages::UNIFORM, ) .unwrap(); + let prediction = Tensor::scalar( + backend, + all_sim_params[0].prediction_distance(), + BufferUsages::UNIFORM | BufferUsages::COPY_DST, + ) + .unwrap(); // Two-element readback: the (max) collision-pair count and the uncolored count. let resize_readback = GpuReadback::new(backend, 2).unwrap(); let collision_pairs_indirect = @@ -741,6 +747,7 @@ impl RbdState { collision_pairs_len, collision_pairs_len_max, num_batches_uniform, + prediction, resize_readback, collision_pairs_indirect, contacts_per_batch_cpu, diff --git a/src_rbd/pipeline/rbd_step.rs b/src_rbd/pipeline/rbd_step.rs index 64817d2..5df9d98 100644 --- a/src_rbd/pipeline/rbd_step.rs +++ b/src_rbd/pipeline/rbd_step.rs @@ -197,6 +197,7 @@ impl RbdPipeline { &state.batch_indices, &state.collider_parent, &state.collider_materials, + &state.prediction, )?; drop(pass); diff --git a/src_rbd_shaders/broad_phase/narrow_phase.rs b/src_rbd_shaders/broad_phase/narrow_phase.rs index 20a4ead..aa10985 100644 --- a/src_rbd_shaders/broad_phase/narrow_phase.rs +++ b/src_rbd_shaders/broad_phase/narrow_phase.rs @@ -70,7 +70,6 @@ pub fn gpu_narrow_phase_init_contacts_dispatch( *indirect_args.at_mut(2) = 1; } -const PREDICTION: f32 = 2.0e-3; // TODO: make the prediction configurable. /// Narrow phase, pass 1 of 2: analytic shape-shape contacts for ball / cuboid /// pairs, written straight into the `contacts` buffer. @@ -95,6 +94,8 @@ pub fn gpu_narrow_phase_shape_shape( #[spirv(storage_buffer, descriptor_set = 0, binding = 7)] collider_parent: &[u32], #[spirv(storage_buffer, descriptor_set = 0, binding = 8)] collider_materials: &[ColliderMaterial], + // Contact prediction distance (`RbdSimParams::prediction_distance`). + #[spirv(uniform, descriptor_set = 0, binding = 9)] prediction: &f32, ) { let num_threads = num_workgroups.x * WORKGROUP_SIZE; let batch_id = invocation_id.y; @@ -162,12 +163,12 @@ pub fn gpu_narrow_phase_shape_shape( if shape_ty1 == SHAPE_TYPE_CUBOID && shape_ty2 == SHAPE_TYPE_CUBOID { let cuboid1 = shape1.to_cuboid(); let cuboid2 = shape2.to_cuboid(); - manifold = cuboid_cuboid(pose12, &cuboid1, &cuboid2, PREDICTION); + manifold = cuboid_cuboid(pose12, &cuboid1, &cuboid2, (*prediction)); } // Everything else (PFM / trimesh / polyline) is handled by the deferred // pass; `manifold.len` stays 0 here so nothing is written. - if manifold.len > 0 && manifold.points_a.at(0).dist < PREDICTION { + if manifold.len > 0 && manifold.points_a.at(0).dist < (*prediction) { let target_contact_index = atomic_add_u32(contacts_len, 1) as usize; // NOTE: if we exceed the contacts allocation size, just skip @@ -212,6 +213,7 @@ pub fn gpu_narrow_phase_shape_shape_deferred( // And we assume all batch dimensions are given the same buffer allocation sizes // (i.e. the same `contacts_batch_capacity`). #[spirv(uniform, descriptor_set = 0, binding = 6)] batch_ids: &BatchIndices, + #[spirv(uniform, descriptor_set = 0, binding = 7)] prediction: &f32, ) { let num_threads = num_workgroups.x * WORKGROUP_SIZE; let batch_id = invocation_id.y; @@ -296,6 +298,7 @@ pub fn gpu_narrow_phase_shape_shape_deferred( let mesh = shape1.to_trimesh(); let convex = shape2; trimesh_convex( + *prediction, pose12, &mesh, convex, @@ -313,6 +316,7 @@ pub fn gpu_narrow_phase_shape_shape_deferred( let mesh = shape2.to_trimesh(); // NOTE: pair indices are flipped. trimesh_convex( + *prediction, pose12.inverse(), &mesh, convex, @@ -331,6 +335,7 @@ pub fn gpu_narrow_phase_shape_shape_deferred( let pline = shape1.to_polyline(); let convex = shape2; polyline_convex( + *prediction, pose12, &pline, convex, @@ -348,6 +353,7 @@ pub fn gpu_narrow_phase_shape_shape_deferred( let pline = shape2.to_polyline(); // NOTE: pair indices are flipped. polyline_convex( + *prediction, pose12.inverse(), &pline, convex, @@ -364,6 +370,7 @@ pub fn gpu_narrow_phase_shape_shape_deferred( /// Collision detection between a triangle mesh and a convex shape. fn trimesh_convex( + prediction: f32, pose12: Pose, mesh: &TriMesh, convex: &Shape, @@ -379,10 +386,10 @@ fn trimesh_convex( return; } - // Get the convex shape's AABB in the trimesh's local space, and enlarge with the PREDICTION. + // Get the convex shape's AABB in the trimesh's local space, and enlarge with the prediction distance. let mut test_aabb = convex.compute_aabb(pose12, vertices); - test_aabb.mins -= Vector::splat(PREDICTION); - test_aabb.maxs += Vector::splat(PREDICTION); + test_aabb.mins -= Vector::splat(prediction); + test_aabb.maxs += Vector::splat(prediction); if !test_aabb.intersects(&mesh.root_aabb) { // No collision possible. @@ -430,6 +437,7 @@ fn trimesh_convex( /// Collision detection between a polyline and a convex shape. fn polyline_convex( + prediction: f32, pose12: Pose, mesh: &Polyline, convex: &Shape, @@ -445,11 +453,11 @@ fn polyline_convex( return; } - // Get the convex shape's AABB in the polyline's local space, and enlarge with the PREDICTION. + // Get the convex shape's AABB in the polyline's local space, and enlarge with the prediction distance. let thickness = 0.4; // TODO: make thickness configurable or part of the polyline struct let mut test_aabb = convex.compute_aabb(pose12, vertices); - test_aabb.mins -= Vector::splat(PREDICTION + thickness); - test_aabb.maxs += Vector::splat(PREDICTION + thickness); + test_aabb.mins -= Vector::splat(prediction + thickness); + test_aabb.maxs += Vector::splat(prediction + thickness); if !test_aabb.intersects(&mesh.root_aabb) { // No collision possible. @@ -555,6 +563,7 @@ pub fn gpu_narrow_phase_pfm_pfm( #[spirv(storage_buffer, descriptor_set = 0, binding = 7)] collider_parent: &[u32], #[spirv(storage_buffer, descriptor_set = 0, binding = 8)] collider_materials: &[ColliderMaterial], + #[spirv(uniform, descriptor_set = 0, binding = 9)] prediction: &f32, ) { let num_threads = num_workgroups.x * WORKGROUP_SIZE; let batch_id = invocation_id.y; @@ -583,13 +592,13 @@ pub fn gpu_narrow_phase_pfm_pfm( pair.thickness1, &pair.shape2, pair.thickness2, - PREDICTION, + (*prediction), vertices, #[cfg(feature = "dim3")] indices, ); - if manifold.len > 0 && manifold.points_a.at(0).dist < PREDICTION { + if manifold.len > 0 && manifold.points_a.at(0).dist < (*prediction) { let target_contact_index = atomic_add_u32(contacts_len, 1) as usize; // NOTE: if we exceed the contacts allocation size, just skip From d56a271b20399155c423b0dbfae80feb335a39a9 Mon Sep 17 00:00:00 2001 From: haixuantao Date: Fri, 7 Aug 2026 12:10:04 +0200 Subject: [PATCH 2/2] fix ci: remove unnecessary parens around *prediction + rustfmt Co-Authored-By: Claude Fable 5 --- src_rbd_shaders/broad_phase/narrow_phase.rs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src_rbd_shaders/broad_phase/narrow_phase.rs b/src_rbd_shaders/broad_phase/narrow_phase.rs index aa10985..4fd8d43 100644 --- a/src_rbd_shaders/broad_phase/narrow_phase.rs +++ b/src_rbd_shaders/broad_phase/narrow_phase.rs @@ -70,7 +70,6 @@ pub fn gpu_narrow_phase_init_contacts_dispatch( *indirect_args.at_mut(2) = 1; } - /// Narrow phase, pass 1 of 2: analytic shape-shape contacts for ball / cuboid /// pairs, written straight into the `contacts` buffer. /// @@ -163,12 +162,12 @@ pub fn gpu_narrow_phase_shape_shape( if shape_ty1 == SHAPE_TYPE_CUBOID && shape_ty2 == SHAPE_TYPE_CUBOID { let cuboid1 = shape1.to_cuboid(); let cuboid2 = shape2.to_cuboid(); - manifold = cuboid_cuboid(pose12, &cuboid1, &cuboid2, (*prediction)); + manifold = cuboid_cuboid(pose12, &cuboid1, &cuboid2, *prediction); } // Everything else (PFM / trimesh / polyline) is handled by the deferred // pass; `manifold.len` stays 0 here so nothing is written. - if manifold.len > 0 && manifold.points_a.at(0).dist < (*prediction) { + if manifold.len > 0 && manifold.points_a.at(0).dist < *prediction { let target_contact_index = atomic_add_u32(contacts_len, 1) as usize; // NOTE: if we exceed the contacts allocation size, just skip @@ -592,13 +591,13 @@ pub fn gpu_narrow_phase_pfm_pfm( pair.thickness1, &pair.shape2, pair.thickness2, - (*prediction), + *prediction, vertices, #[cfg(feature = "dim3")] indices, ); - if manifold.len > 0 && manifold.points_a.at(0).dist < (*prediction) { + if manifold.len > 0 && manifold.points_a.at(0).dist < *prediction { let target_contact_index = atomic_add_u32(contacts_len, 1) as usize; // NOTE: if we exceed the contacts allocation size, just skip