Capacities and watermarks
Every buffer has a fixed size so the step can be captured. Rows are stored per world in three families:
| Family | Option (default) | Holds |
|---|---|---|
| Articulation | articulation_ (32) |
Joint limits, velocity limits, joint friction, drive rows, mimic and loop rows, dense torsion rows; under the immediate response also every contact of an articulated body; under propagation, contacts within one articulation |
| Free body | free_ (512) |
Free-body contacts under the immediate response; free-body velocity-limit rows |
| Propagation | propagation_ (sum of the other two) |
Every contact under the propagation response except same-articulation ones, plus a torsion row per contact with a torsion radius |
Rows that do not fit are dropped, and the world is flagged in solver.constraint_overflow, a device boolean array with one entry per world plus a final entry for global articulations. The flags persist until reset; read them in a kernel (for example as an RL termination term) or call check_constraint_capacity() outside capture. A dropped row is not always visible: a scene can look right while its rows are silently missing.
Which rows survive an overflow depends on the order of reservation. By default contacts reserve slots with atomic counters, in the order threads arrive and in the collision pipeline’s contact order, and both change from step to step, so a different set of contacts keeps its rows on each step. A contact that penetrates while dropped is pushed out by position correction once it gets rows back, which adds energy: a folding twelve-link chain that needed about 96 articulation rows at the default capacity of 32 went non-finite in 4 of 6 runs with the default immediate response (RTX PRO 6000). With deterministic=wp.DeterministicMode.RUN_TO_RUN and CollisionPipeline(deterministic=True), the earliest contacts in buffer order keep their rows and the selection repeats for the same inputs. In the folding chain the kept set then changed much less between steps (0.5% of kept self-contact pairs per step, against about a third), and repeated runs matched bitwise; that helps reproduce an overflow, not fix it. Size the capacities so nothing overflows. Solver documentation of this is in review in skild-ai/newton#21.
Construct with row_watermark=True, run the whole episode, and read constraint_row_watermarks(). Size from *_raw_high_water (requested rows), never from *_high_water, which stops at the capacity. While a family overflows, the requested mark undercounts, because dropped contacts change the motion: raise it to about twice the mark, rerun, then keep 1.3× headroom over a mark measured without overflow.
Inputs are grouped by where rows come from; bars compare each family with its default capacity, and the grid below shows the articulation buffer of one world filling up. An upper-bound estimate, not a simulation: measure with row_watermark=True. Tutorial: Row capacity and overflow
Velocity-limit counts assume velocity_limit_activation_fraction=0, where every limited DOF or free-body axis takes two rows each step (free-body speed limits come from the steel:rigid_body_max_*_velocity attributes). The defaults above describe one arm with a mimic gripper holding a box on a table: two fingers with four contacts each, four box-table contacts and two DOFs near a limit already use most of the 32 articulation rows. Each contact is counted at three rows (normal and two friction rows); friction patches often need fewer, because contacts away from an anchor keep only their normal row. In a measured three-box stack, 20 contacts took 60 free-body rows with point friction and 40 with patches. With the default speed-aware limit gap, a limit row exists only while its joint is near the limit; with joint_limit_activation_gap=inf every finite limit takes a row each step, two per DOF limited on both sides.
Steel docs and tutorials are licensed under Apache-2.0. Third-party notices. "Skild", "Skild AI" and the Skild logo are trademarks of Skild AI and are not licensed under Apache-2.0.