Stable contact identifiers#706
Closed
Jondolf wants to merge 8 commits into
Closed
Conversation
Jondolf
added a commit
that referenced
this pull request
Jul 7, 2025
# Objective A big step in #319. Replaces #706. Currently, Avian's constraint solver is entirely single-threaded and uses scalar operations. A naive `par_iter` approach is not possible with a Gauss-Seidel solver without race conditions. One form of solver parallelism is *[inter-island parallelism](https://box2d.org/posts/2023/10/simulation-islands/)*, meaning that bodies are assigned to islands based on the constraint graph, and each island can be safely solved in parallel, since they are guaranteed to be fully disjoint. Rapier uses this for its solver. However, inter-island parallelism does not help with large islands, such as large piles of bodies. Constraints within each island must still be solved serially. Additionally, it does not help in vectorizing the constraint solver via wide SIMD. Simulation islands are therefore not enough for the performance we are aiming for (though we do still need them for sleeping and waking!). State-of-the-art physics solvers instead use various forms of *[graph coloring](https://en.wikipedia.org/wiki/Graph_coloring)*. The idea is that each constraint is assigned a color such that no adjacent edge shares the same color. This ensures that each body only appears within each color once at most, and all constraints within a given color can be safely solved in parallel without race conditions. This not only makes it trivial to solve constraints with multi-threading, but also opens the door to solving constraints with wide SIMD. The scope of this PR is to implement a graph coloring algorithm for contact constraints, and to make any related important changes around contact management. Wide SIMD and joint graph coloring are left to follow-ups. ## Solution - Rework the `ContactGraph` to work better with graph coloring and optimize contact management. - The graph is now a `StableUnGraph` that stores `ContactEdge`s with stable `ContactId`s. Contact edges store cold connectivity data. - Store the actual contact pairs in separate `active_pairs` and `sleeping_pairs` vectors. Only active pairs are considered in graph coloring and iterated in the narrow phase. - Move `ContactConstraint` preparation out of the narrow phase and into the solver. - Implement a `ConstraintGraph` for greedy graph coloring. - Each `ContactManifold` has its own constraint, so the coloring is done at the manifold-level. - Each `GraphColor` stores `ContactManifoldHandle`s to map to contact manifolds in the `ContactGraph`. - Each `ContactEdge` stores `ContactConstraintHandle`s to map to contact constraints in the `ConstraintGraph`. - Graph colors use `BitVec`s to track which bodies are assigned to them. ### References The original design was largely based on Box2D's graph coloring implementation. However, unlike Box2D, we can have multiple contact manifolds per contact pair (for e.g. triangle meshes or other composite shapes), so we need to perform coloring at the manifold-level, which complicates things a bit. That part is my own design. Additionally, we store contact pairs in the contact graph instead of the constraint graph. Some useful references: - [Graph coloring](https://en.wikipedia.org/wiki/Graph_coloring) - [High-Performance Physical Simulations on Next-Generation Architecture with Many Cores][Intel paper] - [Box2D - SIMD Matters] by [Erin Catto] [Intel paper]: http://web.eecs.umich.edu/~msmelyan/papers/physsim_onmanycore_itj.pdf [Box2D - SIMD Matters]: https://box2d.org/posts/2024/08/simd-matters/ [Erin Catto]: https://github.com/erincatto ## Performance Running the Large Pyramid 2D benchmark with this branch, the `main` branch, and `bevy_rapier2d` 0.30 (configured to match Avian), we get the following results:  This means that with graph coloring, Avian is now faster than Rapier for this scene! Rapier's profile is very flat, as it does not have solver parallelism for large islands. Note that Rapier is still generally faster than Avian for scenes that actually benefit from inter-island parallelism. For example, the Many Pyramids 2D benchmark looks like this:  However, judging by the shapes of the graphs, our multi-threaded scaling still seems better. I think Avian is slower here not because of the solver, but because of other less optimized parts, like the broad phase and spatial query pipeline updates. The upcoming BVH broad phase and spatial query rework should largely resolve this. Here you can see a more detailed breakdown of Avian's performance on the `main` branch:  And now with graph coloring, on this branch:  At the top of the profile you can see the contacts being assigned to graph colors. You can see how the narrow phase is now significantly faster, and more importantly the solver is over **3x as fast** on my machine, resulting in nearly **2x total performance** compared to `main`. This difference tends to grow the more contacts there are. Something that is not shown in this profile is how contacts are now split into separate sets for active and sleeping bodies. It means that the narrow phase can entirely skip iteration over sleeping contacts, which may be a noticeable improvement for larger scenes with lots of sleeping bodies. ## Future Work - Wide SIMD for contacts - Graph coloring for joints - Simulation islands for sleeping and waking - Split sensor logic to be separate from the narrow phase --- ## Migration Guide ### Contact graph - The `ContactGraph` now stores `ContactEdge`s in the graph, containing only edge connectivity information with stable `ContactId`s. - The `ContactGraph` now stores `ContactPair`s in separate `active_pairs` and `sleeping_pairs` lists. - `iter` and `iter_mut` have been renamed to `iter_active` and `iter_active_mut` / `iter_sleeping` and `iter_sleeping_mut`. - `iter_touching` and `iter_touching_mut` have been renamed to `iter_touching_active` and `iter_touching_active_mut` / `iter_touching_sleeping` and `iter_touching_sleeping_mut`. - `collisions_with` has been renamed to `contact_pairs_with`. - `collisions_with_mut` has been removed for now. - `add_pair` has been replaced by `add_edge`, and `add_pair_with_key` has been replaced by `add_edge_and_key_with`. - `insert_pair` and `insert_pair_with` have been removed. Add or update pairs manually instead. - `remove_pair` has been renamed to `remove_edge`. ### Contact types - `ContactPair` now stores the `ContactId` associated with the `ContactEdge` in the `ContactGraph`. It must be provided to `ContactPair::new`. It is initialized automatically by calls to methods like `ContactGraph::add_edge`. - `ContactPairFlags` no longer stores whether `CONTACT_EVENTS` are enabled. It is instead stored on `ContactEdgeFlags`. - `ContactManifold` no longer stores the `index` of the manifold. ### Determinism The `ContactGraph` uses a `StableUnGraph` with stable indices. A side effect of this is that the order of old contacts can affect where new contacts are added. Thus, if you reset a scene without clearing the contact graph, you may notice slightly different behavior. For consistent, deterministic behavior when resetting scenes, consider calling `Collisions::clear` to also reset the contact graph and make sure physics starts with a clean slate.
Member
Author
|
Done as a part of #771 :) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Objective
Currently, contact pairs have no stable indentifiers. Contact pairs (i.e. graph edges) are removed with
swap_remove, which can invalidate edge indices.This causes a few problems:
For both of these cases, we would benefit from stable identifiers that can be used to efficiently fetch or remove contact pairs from the contact graph without invalidating other existing identifiers.
Solution
Rework the
UnGraphdata structure andContactGraphto support stable identifiers for graph edge connectivity data.UnGraphnow stores a separateVecfor edges and edge weights. AnEdgeonly contains edge connectivity data and the index of the weight associated with it, orNoneif the edge is vacant. The order of edges is stable, and a free list is used to track which edges are vacant, but edge weights are still allowed to be moved around by edge removal.This is a sort of mix of petgraph's
UnGraphandStableUnGraph. For contacts, we need stable edge indices and fast iteration over edge weights, butStableUnGraphonly provides the former, as it also stores edge weights for vacant edges. By allowing edge weights to be swap-removed and storing vacant edges for connectivity data to keep their order stable, we (hopefully) get the best of both worlds.The
ContactGraphuses this new graph representation, and stores a stableContactIdin eachContactPairandContactConstraint. This lets us fix the pair removal problem (1) and improve the performance of writing back impulses from constraints to contacts (2).I also otherwise cleaned up
UnGraphand removed some unused code from it.Comparison With Box2D
A lot of Avian's narrow phase design (e.g. using bit sets for status changes) is inspired by Box2D v3. The core idea for this PR also came from Box2D.
b2ContactArraystoresb2Contacts, which only contain cold data such as the contact index, the previous and next contact edge, island connectivity information, and more. This is similar to theNodes andEdges in ourUnGraph, and has a stable contact ID.b2ContactSimArraystoresb2ContactSims, which contain the actual contact pair data, and the ID of the correspondingb2Contact. This is similar to theContactPairedge weights in ourUnGraph, and does not have stable indices, as the items are swap-removed when removing contact pairs.contactIdPoolis used to manage IDs and add items to the correct slots in theb2ContactArray. OurUnGraphdoesn't store a separate ID pool; instead, it just uses a free list and marks edges as vacant or occupied.While Box2D keeps these structures separate, we have most of the complexity and logic contained within the
UnGraphbehind a convenient and flexible API.Results
Before, "Store Impulses" had a fairly substantial cost in contact-heavy scenarios.
Now, it takes less than half the time:
(note that these profiles were done on a branch with huge solver optimizations; on the main branch, the solver is much more expensive here)
Contact pair removal should also be much more efficient, but that is harder to profile directly. In addition to the performance wins, the new stable identifiers clean up some logic quite nicely.
Todo