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)
- 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.
- 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.
- 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
Symptom
Any scene where two
FIXEDcolliders overlap — the most ordinary example being a large ground cuboid with a terrainTriMeshlaid 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 withCUDA_ERROR_ILLEGAL_ADDRESSbefore 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 explainscollider_parentis 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)
shape_shape) and pass-2 (shape_shape_deferred) already resolvecollider_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.groups_i.test(groups_j)check catches it with zero extra bandwidth — nocollider_parentneeded, at the cost of one reserved bit.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