Limits and capacities

Magnitude limits

Every value the API accepts is finite and within a limit, so that every product the step forms stays in float range. The limits are public constants in maul3d/base.h, beside the soft body, vehicle and drivetrain counts in their headers. A call past a limit refuses with m3_errorLimit (a create returns the null id), counts one misuse unless the world's state decides it (see which refusals count misuse), journals nothing and changes nothing; replay refuses the same values in a recorded op. A NaN is invalid rather than past a limit, and returns m3_errorInvalid; an infinity is past every limit. Each limit sits far past what real scenes use. Vectors are bounded per component.

Quantity Limit Where
Position M3_POSITION_LIMIT, 1e15 m body defs, poses, anchors in the world, a plane's offset, water boxes, every soft particle a create places
Body-frame offset or extent M3_LOCAL_LIMIT, 1e4 m shape geometry, joint anchors, wheel anchors and lengths, a servo's aim, soft spacing and particle radius, a character's radius and half height
Kinematic target M3_TARGET_REACH_LIMIT, 1e6 m from the kinematic body m3Body_SetTargetTransform
Static geometry M3_GEOMETRY_LIMIT, 1e8 m in its body's frame mesh vertices, height samples
Cell size M3_CELL_LIMIT, 1e5 m, positive height field grids and mesh height fields, voxel chunks
Cast or character move M3_CAST_LIMIT, 1e18 m shape casts, m3Character_Move
Speed M3_SPEED_LIMIT, 1e5 m/s and rad/s dynamic body velocities, every body's spin, the world's speed caps, contact push speed, wind speed, water flow, surface velocity, joint motor speed
Kinematic speed M3_KINEMATIC_SPEED_LIMIT, 1e10 m/s kinematic body linear velocities
Impulse the velocity it leaves within M3_SPEED_LIMIT m3Body_ApplyLinearImpulse, m3Body_ApplyAngularImpulse, m3Body_ApplyLinearImpulseAtPoint
Force and torque M3_FORCE_LIMIT, 1e20 N and N m, held or added up over a step m3Body_ApplyForce, m3Body_ApplyTorque, m3Body_ApplyForceAtPoint, joint motor and steering effort, a servo's budgets, vehicle drive and brake force, engine torque
Acceleration M3_ACCEL_LIMIT, 1e4 m/s^2 gravity
Frequency M3_HERTZ_LIMIT, 1e5 Hz contact hertz, joint springs and steering, wind gusts, wheel suspension
Ratio M3_RATIO_LIMIT, 1e4 damping ratios, restitution, gravity scales, gear and pulley ratios, gear and final drive ratios, brake share, tire grip, lean gain, wind gust scale, a character's push ratio, an explosion's soft push
Differential coupling M3_FORCE_LIMIT / M3_SPEED_LIMIT, 1e15 N per m/s m3DrivetrainDef.differentialCoupling
Step length M3_MIN_STEP to M3_STEP_LIMIT, 1e-4 to 10 s m3World_Step
Substeps M3_MAX_SUBSTEPS, 256 m3World_Step
Density M3_DENSITY_LIMIT, 1e6 kg/m^3 shapes, water
Mass a shape at least M3_MIN_MASS, 1e-6 kg, with every principal moment at least M3_MIN_INERTIA, 1e-12 kg m^2; a body at most M3_MASS_LIMIT, 1e20 kg shapes on dynamic bodies, a body turning dynamic, characters, soft particles
Wheel radius at least M3_MIN_WHEEL_RADIUS, 1e-3 m vehicle wheels
Character stance M3_MIN_STANCE to M3_STANCE_LIMIT, 0.05 to 10 m m3Character_SetStance
Explosion impulse M3_EXPLOSION_IMPULSE_LIMIT, 1e10 N s/m^2 m3ExplosionDef.impulsePerArea
Joint lever M3_JOINT_LEVER_LIMIT, 1e10 for mass * arm^2 / moment: see below a spherical, revolute, prismatic, weld, wheel or motor joint, a shape create, destroy, resize or density change on its bodies, a body's type change
Contact lever M3_CONTACT_LEVER_LIMIT, 1e6 for mass * arm^2 / moment: see below a shape create, destroy, resize, density or filter change on a dynamic body, a body turning dynamic
Counts M3_MAX_HULL_POINTS 256, M3_MAX_CAST_POINTS 64, M3_MAX_MESH_VERTICES and M3_MAX_MESH_TRIANGLES 65535, M3_MAX_MESH_MATERIALS 8, M3_MAX_HEIGHTFIELD_SIZE 255, M3_MAX_HEIGHTFIELD_MESH_SIZE 32, M3_MAX_VOXEL_SURFACE_BOXES 2048, M3_MAX_BODY_NAME_BYTES 31 hull clouds, casts, meshes and their materials, height field grids and mesh height fields, a voxel chunk's surface, a body's name
Capacities M3_MAX_SHAPE_CAPACITY, M3_MAX_PAIR_CAPACITY and M3_MAX_VOXEL_CAPACITY m3WorldDef

The step holds what it produces within the same limits: every body leaves a substep within 1e5 m/s and 1e5 rad/s, a bounce asks for at most 1e5 m/s of separation, and a push the step itself lands on a body (a character's, a soft particle's, a wheel's, an explosion's) leaves it within 1e5 as well. A character's, a soft particle's or a wheel's push whose float sum overflows is taken again in double, and m3Counters.heldPushRetakeCount counts it. A stored contact or joint impulse is held, direction kept, so its warm start changes a body's velocity or spin by at most 1e13 per component at that step's own masses and levers.

One limit spans two bodies. Take a joint's bodies, the arm from each one's center of mass to its anchor, and the moment about the axis that arm turns it: the joint's lever ratio is their mass times the squared arm over that moment, summed as the turn each gives over the inverse masses together, so a static or kinematic body adds nothing. A joint that solves a point block or a pair of held rows, the spherical, revolute, prismatic, weld, wheel and motor joints, keeps it within 1e10. Past about 5e11 those blocks, solved one after another, undo each other, and the joint lets its body go or flings it, though each value is within the limits above: a 1.8e4 m rod 0.5 mm thick at density 1e6 on a 1e4 m arm reaches about 2.5e15. A joint create past the ratio is refused, and so is any change that would carry a joint there: a shape created on, destroyed from, resized on or given a new density on one of its bodies, or a body's type change. Each returns m3_errorLimit, changes nothing and counts no misuse, since the bodies' masses decide it, not the argument. Restore and replay refuse it too. The ratio does not depend on how the bodies turn, so a joint within it stays within it as they move. The distance, generic, parallel, gear, pulley and filter joints solve single rows and keep no such limit.

A dynamic body keeps the same ratio for its contacts, over its own mass: its mass times the squared arm from its center of mass to any point of a shape that can collide (not a sensor, and with a nonzero category and mask or a positive group), over the moment that arm turns it, within 1e6. Contacts are solved in float lanes, one constraint after another, so a contact far out on a body that a joint or another contact also holds must be undone by it at every pass: past about 1e7 the two no longer settle, and the contact lets its shape sink through the ground. A core of density 1e6 and radius 1 with a colliding shell of radius 1 reaches 1e6 near 630 m. A shape create, destroy, resize or density change, a filter change that lets a shape collide, and a body turning dynamic are refused past it, each with m3_errorLimit, changing nothing and counting no misuse; restore and replay refuse it too. Shapes that collide with nothing, and static and kinematic bodies, keep no such limit.

Restore refuses the same values past the same limits. Creates and teleports place every body and soft particle within 1e15 m, and the step holds it there, direction kept: a body reaches the bound only by moving at the kinematic limit for about 1e5 s, or at the speed limit for about 1e10 s.

Replay and restore take a shape's geometry only in the form a create leaves it: a field its type does not use is zero, and a plane's normal is unit length.

Capacities

Every pool is fixed at world creation (bodyCapacity, shapeCapacity, meshCapacity, jointCapacity, voxelCapacity, characterCapacity, vehicleCapacity, softBodyCapacity) and never grows. A create past a full pool is refused with m3_errorCapacity and the null id, counts no misuse, and the world keeps stepping.

The broadphase pair table is fixed too: pairCapacity pairs, 8 per shape of shapeCapacity when it is left 0. A dense pile can find more pairs than that. The step then keeps the pairs of the lowest shape indexes, so twins, replays and threaded runs keep the same ones, and drops the rest for that step: the two shapes of a dropped pair get no contact and pass through each other. The step still runs whole for every other body. m3Counters.pairsDropped counts the drops, so check it while a game is in development and raise pairCapacity until it stays at zero:

uint64_t dropped = m3World_GetCounters(world).pairsDropped;
(void)m3World_Step(world, dt, 4);
if (m3World_GetCounters(world).pairsDropped != dropped)
{
    tooFewPairs = true; // give the world a larger m3WorldDef.pairCapacity
}

The world also takes its step scratch at creation, sized for a step at those capacities, so stepping never allocates. m3World_GetMemoryUsage gives the exact sizes (scratchCapacity, persistentBytes), and a world whose scratch would pass 2 GiB, or that the allocator refuses, is not created (m3_errorCapacity).

As a guide for budgets, here is what release 1.0.0 takes on a 64-bit target. The figures were measured on x86-64 Linux with a clang-20 build, by creating worlds whose capacities differ by a known count and comparing their m3World_GetMemoryUsage. Such a world takes about

918 * pairCapacity + 126 * bodyCapacity + 416 * jointCapacity + 57 KiB

bytes of scratch, and each pair also keeps 564 bytes of persistent tables, so the default 8 pairs cost about 11.6 KiB per shape. The default world (1024 bodies, 2048 shapes and 16,384 pairs, 64 joints) takes 14.5 MiB of scratch beside 21.7 MiB of pools and arrays. A later release may change these figures.

Each shape's own ceilings, hulls of 64 vertices from up to 256 input points, meshes of 65,535 vertices and 65,535 triangles, and height fields of up to 255 by 255 samples, are described with the shapes in bodies and shapes.