Broad/narrow phase does not filter fixed–fixed collider pairs — overlapping static geometry explodes the PFM work-list
- Dominant language
- Rust
- Stars
- 115
- Forks
- 6
- Avg merge
- 9h 4m
- Merged PRs (30d)
- 4
Description
## 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.com/claude-code)
https://claude.ai/code/session_01U2n9RqmxTJb8UG5d1Sjw4W
Contributor guide
No contributing guide indexed for this repository
Research direction
Start with lbvh.rs and the pass-1 shape_shape and pass-2 shape_shape_deferred paths described in the issue; trace how collider_parent and fixed/body state reach pair filtering. Done means fixed–fixed pairs are discarded before deferred trimesh per-triangle PFM expansion while fixed–dynamic pairs still proceed. No specific test file is named, so validate against the existing collision and CUDA behavior.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- rust
- Domain
- computer-graphics, performance
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 48/100