Changelog

All notable changes to this project are recorded here. The format follows Keep a Changelog and the project uses Semantic Versioning. Before 1.0.0, any minor release could change the API, the ABI and the snapshot and journal formats. From 1.0.0 every 1.x release keeps the API, and an entry says what each changed result changes.

1.1.0 (2026-11-05)

Changed

Fixed

1.0.5 (2026-11-05)

Fixed

1.0.4 (2026-11-04)

Fixed

1.0.3 (2026-11-04)

Fixed

1.0.2 (2026-11-03)

Fixed

1.0.1 (2026-11-03)

Fixed

1.0.0 (2026-11-03)

Changed

0.13.1 (2026-11-03)

Changed

Fixed

0.13.0 (2026-11-02)

Changed

0.12.0 0.13.0
m3_voxelShape m3_voxelChunkShape
m3Vehicle_GetCompression m3Vehicle_GetWheelCompression
m3SoftBody_AnchorToSoft m3SoftBody_AnchorToSoftBody
m3DrivetrainDef.autoShift enableAutoShift
m3DrivetrainDef.diffMode, diffCouple differentialMode, differentialCoupling
M3_MAX_DRIVETRAIN_CURVE M3_MAX_DRIVETRAIN_CURVE_POINTS
M3_SERVO_REACH_LIMIT M3_TARGET_REACH_LIMIT
M3_BLAST_LIMIT M3_EXPLOSION_IMPULSE_LIMIT

(#344) - Breaking: a dropped explosion carve has its own counter. m3Counters.voxelEditsRefused counted both voxel edits refused with m3_errorLimit and explosion carves m3World_Explode skipped for the same surface bound, which no status reports. It now counts the edits only, and the new m3Counters.voxelCarvesDropped counts the skipped carves. A program that watched voxelEditsRefused for carves reads voxelCarvesDropped. (#346) - Breaking: m3Counters drops three fields other calls report. scratchPeak and scratchCapacity are gone: read them from m3World_GetMemoryUsage. snapshotBytes is gone: call m3World_GetSnapshotSize. m3World_GetCounters no longer measures the snapshot. (#347) - Formats. The snapshot keeps format 6, the journal tape format 7 and the replay container format 4. As in every release, a snapshot, tape or container written by another release is refused with m3_errorVersion.

Fixed

0.12.0 (2026-11-02)

Changed

0.11.0 m3JointDef field 0.12.0
type gone: call the create for your type
lowerLimit, upperLimit spherical lowerTwist, upperTwist; revolute lowerAngle, upperAngle; prismatic and wheel lowerTranslation, upperTranslation
wheel localAxisB localSpinAxisB
genericLinear, genericAngular linearModes, angularModes
genericLinearLower, genericLinearUpper linearLower, linearUpper
genericAngularLower, genericAngularUpper angularLower, angularUpper
genericMotorAxis motorAxis, M3_GENERIC_NO_MOTOR for none

(#299) - Breaking: a dynamic body bounds its contact lever ratio. A colliding shape far from its body's center of mass, on a body a joint also holds, let the contact go: past a ratio of about 1e7, mass times the squared arm over the moment that arm turns, the contact and the joint stopped settling and the body fell through. M3_CONTACT_LEVER_LIMIT, 1e6, is the new bound. A shape create, destroy, resize or density change, a filter change that lets a shape collide, and a type change to dynamic are refused with m3_errorLimit when they would carry the ratio past it. They change nothing and count no misuse. Restore and replay refuse such bytes. m3Shape_SetFilter now returns m3Result, so check its status. (#302) - Breaking: body names are UTF-8 with a byte count. m3Body_SetName(world, body, name, byteCount) returns m3Result. It refuses a name longer than M3_MAX_BODY_NAME_BYTES, 31 bytes, with m3_errorLimit where it cut the name, and bytes that are not well-formed UTF-8 or hold a zero byte with m3_errorInvalid. Each refusal keeps the old name and counts one misuse. A count of 0 clears the name. m3Body_GetName(world, body, nameOut, capacity) copies the name into your buffer with no terminator and returns its byte length; call it with a NULL buffer and capacity 0 to measure. (#293) - Breaking: m3World_Compare returns a status. It takes rowsOut, capacity and totalOut in place of out and outCount, writes up to capacity rows and sets *totalOut to the number of differing slots, which may exceed the capacity. Read min(total, capacity) rows. Worlds of different capacities now compare slot by slot instead of being refused. A refusal returns m3_errorInvalid and counts one misuse, where it returned -1 and counted none. m3EncodeReplay's buffer parameter is named containerOut. (#294) - Breaking: the point test takes a filter, the capsule overlap a center. m3World_TestPoint(world, point, filter) answers with the lowest shape the filter sees; pass m3DefaultQueryFilter() to keep the old answer. m3World_OverlapCapsule takes a center and two points relative to it, as m3World_CastCapsuleClosest does, in place of two world points. (#297) - Breaking: seven internal math helpers leave core_math.h. m3MinF, m3MaxF, m3ClampF, m3AbsF, m3MakeZeroMat3, m3UnwindAngle and m3IntegrateRotation are no longer declared. Use your own. The vector, matrix and quaternion math, m3ComputeCosSin and m3Atan2 stay. (#298) - Formats. The snapshot keeps format 6, the journal tape format 7 and the replay container format 4. As in every release, a snapshot, tape or container written by another release is refused with m3_errorVersion.

Fixed

0.11.0 (2026-11-01)

Added

Changed

0.10.0 0.11.0
m3AabbResult in body.h m3WorldAabb in world.h
m3Joint_GetConstraintForce, GetConstraintTorque m3Joint_GetReactionForce, GetReactionTorque
m3Joint_SetBreakThresholds m3Joint_SetBreakLimits
m3_fixedJoint, value 3 m3_weldJoint, value 3
m3CreateHeightFieldGridShape m3CreateHeightFieldShape
m3CreateHeightFieldShape, the triangle mesh m3CreateHeightFieldMeshShape
m3CreateSoftBodyTet m3CreateTetSoftBody
M3_MOVER_PLANES M3_MAX_MOVER_PLANES
m3World_OverlapHullPoints m3World_OverlapHull
m3FragmentEvent's comChunk, comWorld, boundsLo, boundsHi localCenter, worldCenter, lowerBound, upperBound
m3WaterVolumeDef's lo, hi bounds.lowerBound, bounds.upperBound
m3WheelDef's hertz, zeta springHertz, springDampingRatio
m3Counters' misuse, allocCalls, freeCalls, heldPushRetakes misuseCount, allocCount, freeCount, heldPushRetakeCount
m3Body_GetRotationalInertia m3Body_GetCentralInertia
C# JointType.Fixed JointType.Weld
Godot substeps property substep_count

m3Joint_GetBreakLimits is new and reads back what m3Joint_SetBreakLimits set, 0 for a cap that is off. (#207, #255, #256, #257, #258, #263) - Breaking: the distance joint has its own range, spring and rest length. Its range is m3JointDef.minLength and maxLength, set with m3DistanceJoint_SetLengthRange and read with GetLengthRange. Its spring is switched by the def's enableSpring with springHertz and springDampingRatio, and by m3Joint_EnableSpring and the spring setters at run time. It pulls toward m3JointDef.length, set with m3DistanceJoint_SetLength, which need not lie inside the range. The joint takes no limit and no motor, so a def that still sets the range through enableLimit or the spring through enableMotor is refused at create, as are the limit and motor calls on it. The def's spring fields now switch the spring at create on every joint type that takes one. The C# JointDef gains MinLength, MaxLength, Length, EnableSpring, SpringHertz and SpringDampingRatio. (#191, #202) - Breaking: kinds and modes are fixed-width integers. m3BodyType, m3ShapeType, m3JointType and m3AxisMode are int32_t typedefs with the same named values, so source that uses the names compiles as before. m3DrivetrainDef.diffMode is an m3DifferentialMode: m3_differentialOpen, m3_differentialLimitedSlip or m3_differentialLocked. The generic joint's modes are m3AxisMode arrays, so m3JointDef grows from 248 to 272 bytes. The C# defs hold BodyType, JointType and a new AxisMode enum. (#282) - Breaking: refusals a correct program can meet return a status. These calls carry M3_NODISCARD and return m3Result: - m3VoxelChunkShape_SetVoxel, ClearVoxel, SetFill and ClearBox return m3_errorLimit when the chunk's surface would pass M3_MAX_VOXEL_SURFACE_BOXES, 2048 boxes, and change nothing. ClearBox writes its count through a new int32_t* clearedOut. - m3SoftBody_PinParticle, AnchorParticle and AnchorToSoft return m3_errorLimit for a full table of M3_MAX_SOFTBODY_ANCHORS. - m3Body_SetType, m3Body_SetTargetTransform, m3Shape_SetDensity, m3Shape_SetSphere and m3Shape_SetCapsule return m3_errorLimit for a body mass past M3_MASS_LIMIT, a joint past its lever ratio, a type change the body's shapes, joints or speed refuse, or a servo target out of reach.

A refusal the world's state decides no longer counts misuse, in these calls and in the shape and joint creates and m3DestroyShape. A stale id is m3_errorStale, a density past M3_DENSITY_LIMIT m3_errorLimit and a bad argument m3_errorInvalid, each still counting one misuse. (#265, #266, #268) - Breaking: m3World_Step returns m3_errorLimit past its published bounds. A dt below M3_MIN_STEP or above M3_STEP_LIMIT, an infinite one included, and more than M3_MAX_SUBSTEPS substeps were m3_errorInvalid. A NaN dt, a dt of zero or below and fewer than one substep stay m3_errorInvalid. Each still counts one misuse. The step's second parameter is named substepCount. (#283, #263) - Breaking: list calls take the filter before the array and return the true total. m3World_OverlapAabb, OverlapSphere, OverlapCapsule, OverlapBox, OverlapHull and CastRayAll take the query filter before the output array. m3World_OverlapAabb takes one m3WorldAabb in place of its two corners. Those six, m3World_CollideMover, m3Body_GetContactData and m3Shape_GetContactData write at most capacity entries and return the total, which may exceed the capacity, where they returned the count written. A NULL array with capacity 0 measures. A NULL array with a positive capacity, or a negative capacity, is refused with one misuse by these calls and by m3World_GetBodies, GetJoints, m3Body_GetShapes and GetJoints, which took both before. (#207, #216) - Breaking: a refused list call or snapshot zeroes the caller's buffer. Every list call above and m3World_Compare zero their array over its whole capacity when they refuse, a stale id included, so an earlier answer is never read as this one. A successful answer, an empty one included, writes only its entries. A refused m3World_Snapshot zeroes its buffer over its capacity, so a host that rings snapshot buffers should size each with m3World_GetSnapshotSize first. (#259) - Breaking: one hit type for every cast. m3CastHit {shapeId, point, normal, fraction} replaces m3RayCastResult and m3RayHit. m3World_CastRayClosest, CastSphereClosest, CastCapsuleClosest, CastBoxClosest, CastHullClosest and CastMover return bool and take m3CastHit* hitOut last. Each clears it first, so a miss leaves zeros, and a NULL hitOut returns false with one misuse. m3World_CastRayAll fills m3CastHit. The C# RayCastResult is CastHit, and CastRayClosest takes out CastHit hit. (#211) - Breaking: sensor events name the sensor, the visitor and the step. m3SensorEvents holds arrays of m3SensorBeginEvent and m3SensorEndEvent, each {sensorShapeId, visitorShapeId, step}, in place of the contact event structs with shapeIdA and shapeIdB. step is the world's step count before the step, so the first step's events carry 0. (#217) - Breaking: snapshot, journal and replay sizes are 64-bit, with no cap. m3World_GetSnapshotSize, m3World_Snapshot, m3World_Restore, m3World_StartJournal, m3World_StopJournal, m3World_ReplayJournal, m3DescribeJournal, m3GetEncodedReplaySize, m3EncodeReplay and m3DecodeReplay take and return int64_t sizes, as do m3ReplayView's sizes, m3JournalInfo's counts and m3Counters.snapshotBytes, and the C# snapshot calls take long. M3_MAX_SNAPSHOT_BYTES is removed, and m3CreateWorld no longer limits mesh and hull slots, and a height field grid's create no longer refuses samples, to keep a snapshot under 2 GiB. On a 32-bit target a size past SIZE_MAX is m3_errorInvalid. (#226) - Breaking: what a failed call leaves in bytesOut. m3World_Snapshot and m3World_StopJournal write 0 when the world is NULL, where they left it. A stop refused as busy, from the world's own callback, leaves it as it was, where it wrote 0. m3EncodeReplay leaves it as it was on any failure, where it wrote 0. (#210, #215) - Breaking: m3IsCpuBackendSupported returns bool in place of an int32_t 1 or 0, and m3Hash64 takes an int64_t byte count. (#260) - Breaking: joints solve their coupled rows as one block. A slider, weld or generic weld whose anchor sits far from its body's center of mass gave way by metres: on a 100 m arm a slider sagged 2.4 m off its axis in a second. It now holds within 1e-4 m. The prismatic, weld, revolute, wheel and generic joints step to different results, and the joint-heavy bench scene costs about 18% more a step, the others about 5%. (#232) - Breaking: m3Counters.awakeCount counts only enabled bodies. A body disabled while awake keeps its awake flag and was counted, though it takes no part in the step. (#287) - Formats. Journal tapes are format 7 and the replay container is format 4, with a 56-byte header. As in every release, a snapshot, tape or container written by another release is refused with m3_errorVersion. (#191, #202, #205, #226, #282)

Removed

Fixed

0.10.0 (2026-10-29)

Added

Changed

0.9.0 0.10.0
m3TaskFn, m3EnqueueTaskFn, m3FinishTaskFn m3TaskFunction, m3EnqueueTaskFunction, m3FinishTaskFunction
m3PreSolveFn m3PreSolveFunction
m3ResultName m3GetResultName
m3CpuSupportsBackend m3IsCpuBackendSupported
m3World_SnapshotSize m3World_GetSnapshotSize
m3Body_Enable and m3Body_Disable m3Body_Enable with true or false
m3BodyDef.isEnabled m3BodyDef.enable
m3Body_SetBullet, m3Body_IsBullet m3Body_EnableBullet, m3Body_IsBulletEnabled
m3BodyDef.isBullet m3BodyDef.enableBullet
m3Shape_EnablePreSolve, m3Shape_IsPreSolveEnabled m3Shape_EnablePreSolveEvents, m3Shape_IsPreSolveEventsEnabled
m3Shape_SetMeshMaterials m3MeshShape_SetMaterials
m3VoxelChunk_SetVoxel, ClearVoxel, ClearBox, SetFill m3VoxelChunkShape_SetVoxel, ClearVoxel, ClearBox, SetFill
m3Joint_SetTargetAngle m3RevoluteJoint_SetTargetAngle
m3Joint_SetTargetRotation m3SphericalJoint_SetTargetRotation
m3Joint_SetSteer see the next item
m3Joint_GetSteerAngle m3WheelJoint_GetSteeringAngle
m3Vehicle_SelectGear m3Vehicle_SetGear
C# Native.ResultName Native.GetResultName
C# Native.SnapshotSize Native.GetSnapshotSize
C# BodyDef.IsBullet BodyDef.EnableBullet
C# BodyDef.IsEnabled BodyDef.Enable

Struct layouts and the snapshot format are unchanged. The journal stays at format 3. (#188) - Breaking: wheel steering has a switch and a setter per value. m3Joint_SetSteer(enable, targetAngle, hertz, zeta, maxEffort) becomes m3WheelJoint_EnableSteering(enable) and four setters: m3WheelJoint_SetTargetSteeringAngle, SetSteeringHertz, SetSteeringDampingRatio and SetMaxSteeringTorque. zeta is now the damping ratio, and the budget is a torque, 0 for no cap. Steering keeps its values while it is off. As before, enabled steering needs a positive frequency. Because the switch is now a call of its own, set the frequency first and then enable steering: enabling it before any frequency is set is refused and counts one misuse. Each call records its own journal entry. A tape that steers is therefore longer than the same scene recorded with 0.9.0, and its bytes hash differently, while the simulation itself is unchanged. (#188)

Fixed

0.9.0 (2026-10-29)

Added

Changed

Fixed

0.8.0 (2026-10-28)

Added

Changed

Fixed

0.7.0 (2026-10-27)

Changed

Fixed

0.6.0 (2026-10-27)

Changed

Added

Fixed

Security

0.5.0 (2026-10-26)

Changed

Fixed

Security

0.4.0 (2026-10-25)

Changed

Fixed

0.3.0 (2026-10-25)

Changed

Fixed

0.2.1 (2026-10-24)

Changed

Fixed

0.2.0 (2026-10-24)

Added

Changed

Every other public struct that holds an aligned member keeps its size and changes only its alignment, from 4 to 8. An i686 tape recorded with 0.1.x is refused with m3_errorInvalid when it holds one of the twelve journal records whose size changed: a body, character, vehicle, water volume or joint create, any shape create, a filter change, or a force or impulse at a point. Other i686 tapes, and every i686 snapshot, still load. On x86-64, arm64, Windows and wasm nothing changes but tape padding (below): existing tapes and snapshots load as before. (#44)

Fixed

0.1.1 (2026-10-24)

Added

Changed

Fixed

0.1.0 (2026-10-24)

Changed

Fixed