How Box3D Organizes Bodies into Static, Awake, and Sleeping Categories Using Solver Sets
Box3D groups bodies, contacts, and joints into contiguous solver sets based on their activity state—static, awake, disabled, or sleeping—to maximize cache locality and skip unnecessary simulation work.
The erincatto/box3d physics engine achieves high performance by organizing simulation data into specialized data structures called solver sets. Instead of checking every body each frame, the engine maintains separate contiguous arrays for static objects, active dynamic bodies, disabled entities, and sleeping islands, allowing the solver to iterate over only relevant data while keeping memory access patterns cache-friendly.
What Are Solver Sets?
A solver set is a data container that keeps related simulation data—bodies, joints, contacts, and islands—together in memory. The structure is defined in [src/solver_set.h](https://github.com/erincatto/box3d/blob/main/src/solver_set.h):
typedef struct b3SolverSet {
b3Array(b3BodySim) bodySims; // all bodies in the set
b3Array(b3BodyState) bodyStates; // only for the awake set
b3Array(b3JointSim) jointSims; // joints (empty for static/awake)
b3Array(int) contactIndices; // contacts (touching for sleeping, non‑touching for awake)
b3Array(b3IslandSim) islandSims; // islands (only for awake & sleeping sets)
int setIndex; // stable id from world->solverSetIdPool
} b3SolverSet;
Box3D maintains four logical categories of solver sets, each serving a distinct purpose in the simulation pipeline.
The Four Solver Set Categories
Static Set
The static set (b3_staticSet) contains all static bodies and any joints that connect only static bodies. Since these objects never move, they require no body state updates or solver iterations. The engine stores them separately to avoid processing them during the simulation step.
Awake Set
The awake set (b3_awakeSet) holds dynamic and kinematic bodies that are currently moving, along with their body states, non-touching contacts, and joints. According to the source code in [src/solver_set.c](https://github.com/erincatto/box3d/blob/main/src/solver_set.c), this is the only set that maintains b3BodyState arrays because it is the only category that requires velocity integration and constraint solving each frame.
Disabled Set
The disabled set (b3_disabledSet) stores bodies explicitly disabled by the user and their associated non-touching contacts. These objects are removed from the solver but preserved in memory for potential re-enabling later.
Sleeping Sets
Sleeping sets (starting from b3_firstSleepingSet) are dynamically allocated—one per sleeping island. When a group of bodies comes to rest, Box3D moves the entire island into its own sleeping set, storing the bodies, touching contacts, and joints together. These sets are excluded from the solver until an external event wakes them.
How Bodies Move Between Solver Sets
Adding Bodies to Sets
When a body is created or its type changes, Box3D determines its target set immediately. In [src/body.c](https://github.com/erincatto/box3d/blob/main/src/body.c) (lines 1554–1559), the logic selects between the static and awake sets:
b3SolverSet* awakeSet = b3Array_Get(world->solverSets, b3_awakeSet);
b3SolverSet* sourceSet = b3Array_Get(world->solverSets, body->setIndex);
b3SolverSet* targetSet = type == b3_staticBody ? staticSet : awakeSet;
b3TransferBody(world, targetSet, sourceSet, body);
The b3TransferBody function, implemented in [src/solver_set.c](https://github.com/erincatto/box3d/blob/main/src/solver_set.c), handles the physical move: it copies the body's simulation data to the destination array, updates internal indices, and creates or destroys a b3BodyState depending on whether the target is the awake set.
Putting Islands to Sleep
When a dynamic island remains idle long enough, b3TrySleepIsland creates a new sleeping solver set and migrates the data. The implementation (lines 70–78 in solver_set.c) allocates a new set ID:
int sleepSetId = b3AllocId(&world->solverSetIdPool);
b3SolverSet* sleepSet = b3Array_Get(world->solverSets, sleepSetId);
*sleepSet = (b3SolverSet){0};
The function then executes the following sequence (documented in lines 91–150):
- Transfers body simulations from the awake set to the new sleeping set
- Moves non-touching contacts to the disabled set
- Moves touching contacts and joints to the sleeping set
- Updates island indices
- Calls
b3DestroySolverSetto free the old awake set resources
Waking Sleeping Sets
When a sleeping island receives a wake-up event—such as a collision with an awake body—b3WakeSolverSet reverses the process (lines 36–115 in solver_set.c):
- Bodies are copied back into the awake set with updated
setIndexandlocalIndexvalues - Non-touching contacts move from the disabled set to the awake set
- Touching contacts are added to the constraint graph and marked as awake
- Joints are inserted into the graph and transferred to the awake set
Merging Solver Sets
If two sleeping sets become linked by a new joint, b3MergeSolverSets consolidates them by moving the smaller set's contents into the larger one. This ensures that connected sleeping bodies remain in the same solver set, allowing them to be woken as a single unit when necessary.
Practical Code Examples
Creating a dynamic body automatically places it in the awake set:
/* Create a dynamic body – it ends up in the awake set */
b3BodyDef def = {0};
def.type = b3_dynamicBody;
b3BodyId id = b3World_CreateBody(world, &def); // internally uses b3TransferBody
Manually forcing sleep for testing purposes:
/* Manually force a body to sleep */
int islandId = b3Body_GetIslandId(id);
b3TrySleepIsland(world, islandId); // moves island to a sleeping set
Waking a body via impulse application:
/* Wake a sleeping island by applying an impulse */
b3Body_ApplyImpulse(id, impulse, point); // calls b3WakeSolverSet internally
Summary
- Box3D solver sets partition the simulation world into four categories: static, awake, disabled, and sleeping.
- The
b3SolverSetstructure insrc/solver_set.hdefines contiguous arrays for bodies, states, joints, contacts, and islands. - Static bodies live in
b3_staticSetand never require state updates. - Awake bodies reside in
b3_awakeSetwith fullb3BodyStatedata for active solving. - Sleeping islands occupy individual sets starting at
b3_firstSleepingSet, excluding them from computation untilb3WakeSolverSetmigrates them back. - Functions like
b3TransferBody,b3TrySleepIsland, andb3MergeSolverSetsmanage migrations while maintaining cache-friendly data layouts.
Frequently Asked Questions
What is the performance benefit of using solver sets in Box3D?
Solver sets improve performance by ensuring that only active bodies are processed each frame. By storing static and sleeping bodies in separate contiguous arrays, the engine avoids cache misses from jumping between active and inactive data, and eliminates branch mispredictions that would occur if every body required an "is active" check during the solver phase.
Why do sleeping bodies get their own individual solver sets rather than sharing one large sleeping set?
Box3D allocates one solver set per sleeping island (a connected group of resting bodies) so that waking can occur at island granularity. When any body in an island receives a collision or impulse, b3WakeSolverSet moves the entire island back to the awake set as a unit. If all sleeping bodies shared one set, the engine would need to filter individual bodies during wake operations, breaking the cache-coherent design.
How does Box3D handle joints that connect bodies in different solver sets?
When a joint connects bodies across set boundaries—such as linking a sleeping body to an awake one—the engine calls b3MergeSolverSets to consolidate the connected islands into a single set. This ensures that constraints are always solved within the same memory block and that the entire group can be woken simultaneously when needed.
What happens to contacts when a body moves from the awake set to a sleeping set?
During the transition handled by b3TrySleepIsland, non-touching contacts (broad-phase pairs not currently colliding) move to the disabled set, while touching contacts (active collisions) migrate to the sleeping set with the bodies. This separation prevents the solver from wasting cycles on speculative contact detection while preserving the contact state for immediate use if the island wakes up.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →