Skip to content

Broad/narrow phase does not filter fixed–fixed collider pairs — overlapping static geometry explodes the PFM work-list #18

Description

@haixuanTao

Symptom

Any scene where two FIXED colliders overlap — the most ordinary example being a large ground cuboid with a terrain TriMesh laid on top of it — generates collision pairs between the two static colliders. For cuboid-vs-trimesh the deferred narrow-phase then expands this into one PFM pair per triangle; with a realistically sized terrain mesh that's ~10⁶ pointless pairs, which overflows/reallocates the pfm work-list and (on the CUDA backend) died for us with CUDA_ERROR_ILLEGAL_ADDRESS before we understood what was happening. Two fixed bodies can never produce a contact response, so all of this work is wasted even when it doesn't crash.

Where

lbvh.rs's pair-emission kernel filters only on collision groups (by design — the comment there explains collider_parent is deliberately kept out of the broad phase, and the same-body skip is deferred to the narrow phase). But the narrow-phase passes don't skip fixed–fixed pairs either, and for trimeshes the damage (per-triangle pfm-pair expansion) is done by the deferred pass before any contact would get discarded downstream.

Workaround we use today

Give every static collider InteractionGroups::new(GROUP_2, ALL ^ GROUP_2) so statics skip each other but still hit dynamic bodies. Works, but it burns a user-visible group bit on an engine-internal concern, and nothing warns the user — the failure mode is an opaque crash or a mysterious slowdown.

Possible fixes (happy to PR whichever direction you prefer)

  1. Narrow-phase skip (fits the existing design): both pass-1 (shape_shape) and pass-2 (shape_shape_deferred) already resolve collider_parent; adding a per-body flags/type read there (new binding, e.g. inv_mass == 0 or a body-type buffer) lets them drop fixed–fixed pairs before the pfm expansion — that's the point where the trimesh blowup is prevented.
  2. Broad-phase skip: fold a "is fixed" bit into the collision-groups word (or the LBVH leaf) so the existing groups_i.test(groups_j) check catches it with zero extra bandwidth — no collider_parent needed, at the cost of one reserved bit.
  3. At minimum: document the behavior and the collision-groups workaround, since overlapping statics are near-universal in robotics scenes (ground plane + terrain, walls + floor).

Context where we hit it: batched RL terrain training (one ground backstop + trimesh terrain strips per world) on the native-CUDA backend, but the pair blowup itself is backend-independent.

🤖 Generated with Claude Code

https://claude.ai/code/session_01U2n9RqmxTJb8UG5d1Sjw4W

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions