Repository navigation
Releases
v1.1.0
1.1.0 (2026-11-05)
Changed
- 1.1.0 refuses snapshots, tapes and containers from 1.0.x. The
snapshot keeps format 6, the journal tape format 7 and the replay
container format 4, but they carryM3_ABI_VERSION, which now names
1.1, so 1.1.0 refuses a snapshot, tape or container that 1.0.x wrote
withm3_errorVersion.m3CreateWorldrefuses a def from 1.0
headers the same way: rebuild against the 1.1 headers. The shared
library'sSOVERSIONis1.1. - A blast inside a shape pushes its body toward its centre of mass.
When the blast ofm3World_Explodelay inside a shape, its rounding
included, the body was pushed from the blast toward a point of that
shape, with the impulse applied there. That point was a sphere's centre,
the middle of a capsule's segment, or the average of a hull's
vertices. A lopsided hull, or a body with several shapes, therefore
spun for no physical reason. A blast exactly at that point pushed
along the shape's own x axis. Now the push runs from the blast to the
body's centre of mass, as maul2d's does, so it spins nothing, and a
blast at the centre of mass itself pushes nothing; the body still
wakes.m3World_Explode, and a replay of a journaled explosion, can
give a different velocity to a body one of whose shapes holds the
blast. A shape the blast is outside of is pushed as before.
(#418)
Fixed
- A mesh create no longer takes seconds on a fan. The edge flags a
mesh bakes on create found each edge's neighbor by scanning every
triangle around one of its vertices, so a fan of triangles around one
vertex took time quadratic in its size: about 5.4 s for 65533
triangles. The bake now takes time linear in the mesh, about 0.025 s
for the same fan, and gives the same flags byte for byte, so no
contact or result changes. The same holds for a mesh create that
m3World_ReplayJournalreplays.
(#413) - Restore refuses a voxel chunk whose cells disagree. An occupied
cell always has a fill of 1 to 255, and an empty cell has fill 0 and
payload 0.m3World_Restorerefused neither an occupied cell with
fill 0 nor an empty cell with a fill or a payload, and
m3World_ReplayJournaltook the same states from a chunk create
record. Both now refuse such a chunk withm3_errorFormat, and
restore leaves the world as it was. No chunk the library writes is
refused.
(#409) - Restore refuses a wheel direction that is not unit length. A
vehicle create takes a wheel direction whose squared length lies
between 0.81 and 1.21 and stores it normalized, but restore took any
stored direction in that band, so a restored wheel could push along
a scaled direction. Restore now requires the squared length within
1e-5 of 1, about 24 times what normalizing leaves, and refuses
anything else withm3_errorFormat.
(#415) - Restore refuses mesh edge flags the mesh would not bake. A mesh's
edge flags are derived from its geometry, and contact welding reads
them, but restore took them as stored. Restore now bakes each mesh's
flags again and refuses a snapshot whose flags differ, with
m3_errorFormat, orm3_errorCapacitywhen it cannot allocate the
bake's scratch. No snapshot the library writes is refused. On the
largest mesh the limits allow, 65522 triangles, a restore took about
26 to 29 ms with the check against 22 ms without it.
(#416)
v1.0.5
1.0.5 (2026-11-05)
Fixed
M3_NODISCARDwarns under GCC 14 in C23 too. The macro marks
every function that returns a status, so that a caller who drops one
gets a warning. In C it took the attribute only when
__STDC_VERSION__was at least 202311L, but GCC 14 reports 202000L in
its C23 mode, so under GCC 14 the macro was empty and nothing warned.
It now takes the attribute in any dialect after C17 whose compiler has
it. A program built with GCC 14 in C23 that drops a status now gets
-Wunused-result, which fails a build with warnings as errors until
the call reads the status or says(void). Clang, C17 and C++ builds
see no change.
(#398)- A body in water never steps to a NaN velocity. Buoyancy sums a
force and a torque over every shape and water volume, from each
shape's bounding box. On a long thin body under several dense
volumes, the torque times the body's inverse inertia could pass float
range, and a large water drag or body damping then turned the
infinite spin into a NaN. A debug build trapped in the speed cap, and
a release build carried the NaN into the world. Now, when a force's or
torque's velocity increment would leave float range, the step takes it
again in double precision and holds it withinM3_SPEED_LIMITalong
its direction. The hold changes no step whose arithmetic stays finite,
however large its forces: such a step computes the same bits with it
as without it. The snapshot and journal formats are unchanged.
(#402) - Replay refuses a voxel fill on an empty cell.
m3VoxelChunkShape_SetFillrefuses an empty cell and records nothing,
butm3World_ReplayJournaltook such a record and reported success,
though the record changed nothing. A tape the engine cannot write
passed as a genuine one. Replay now refuses it withm3_errorFormat
and leaves the world as it was, like any other record the engine never
writes. Tapes the engine wrote replay as before.
(#404) - The library checks every internal yes-or-no answer it computes.
Every internal function that returnsboolnow carries
M3_NODISCARD, so a build of the library warns where its own code
drops such an answer, and the project's checks keep new ones marked.
No public declaration and no result changes, and a program sees
nothing new. The marks found the replay bug above, a voxel fill on an
empty cell.
(#405)
v1.0.4
1.0.4 (2026-11-04)
Fixed
- A static body takes no velocity. A static body never moves, but
m3CreateBodyused to store a static def's velocities, and
m3Body_SetLinearVelocityandm3Body_SetAngularVelocitytook a
static body. The body stayed where it was, but contacts read its
velocity and carried whatever touched it. Create now refuses a static
def whose velocity or spin is not zero (a negative zero counts as
zero) withm3_errorInvalid. The two setters refuse a static body,
whatever the value, and count one misuse. A journal replay refuses
the same calls, so a 1.0.x tape that gives a static body a velocity no
longer replays, andm3World_Restorerefuses a 1.0.x snapshot in
which a static body moves, withm3_errorFormat. Results change only
for a host that gave a static body a velocity or called a velocity
setter on one. A shape'ssurfaceVelocityis the way to make a
conveyor.
(#393) - A force never waits for a later step. A force or torque acts over
the next step and then clears, but a body that stopped moving before
that step used to keep it, and the force acted in full whenever the
body moved again. Such a body was turned static, put to sleep, or
disabled, and a force applied to a disabled body waited the same
way. Nowm3Body_ApplyForce,m3Body_ApplyTorqueand
m3Body_ApplyForceAtPointrefuse a disabled body as misuse, and so
does their replay. A body that stops moving drops the force and
torque it held, so it moves on from rest.m3World_Restorerefuses,
withm3_errorFormat, a snapshot that holds a force or torque on a
body that is not an awake, enabled dynamic body. Besides a stuck
force, that refuses a 1.0.3 snapshot taken after a body with a
pending force turned kinematic and before the next step, which
cleared it. Impulses are unchanged. The step is not changed: a 1.0.3
snapshot that holds neither state nor a moving static body steps to
the same bits, so 1.0.4 restores it.
(#396)
v1.0.3
1.0.3 (2026-11-04)
Fixed
- Restore refuses a sleeping dynamic body that moves. The engine
never writes a dynamic body that is asleep with a linear or angular
velocity other than +0.0, butm3World_Restoreused to accept one
in a forged or damaged snapshot. The restored body then reported its
velocity while asleep and resumed it when woken, where a body put to
sleep through the API starts from rest. Restore now refuses such a
snapshot withm3_errorFormatand leaves the world as it was,
comparing each component bit for bit, so a -0.0 is refused too.
Every snapshot the engine writes restores as before, and the check
leaves stepping as it was.
(#380) - Restore refuses a static or kinematic body that sleeps. Only a
dynamic body falls asleep: create and every type change wake a body,
andm3Body_SetAwakerefuses any other type.m3World_Restoreused
to accept a forged or damaged snapshot with a sleeping static or
kinematic body. It now refuses one withm3_errorFormatand leaves
the world as it was. Every snapshot the engine writes restores as
before, and the check leaves stepping as it was. A 1.0.2 snapshot
steps to the same bits, so 1.0.3 restores it.
(#392)
v1.0.2
1.0.2 (2026-11-03)
Fixed
- A replay leaves no veto for the next live step. A journal tape
cut right after a step's veto record, and before that step, still
replays, since a tape cut at a record boundary is a whole tape. But
it used to leave those vetoes pending: the host's next live step
applied them in place of its own pre-solve callback, journaled none
of them, and two worlds that hashed equal stepped to different bits.
A replay now drops them when it ends, so the next live step calls the
callback. Results change only after replaying a tape that ends right
after a veto record, from the first live step on. A whole session
replays as before, and a 1.0.1 snapshot steps to the same bits, so
1.0.2 restores it.
(#368)
v1.0.1
1.0.1 (2026-11-03)
Fixed
- A journal replay never asks the pre-solve callback. A replayed
step that recorded no veto used to call a callback installed on the
replaying world. A callback that vetoed then made the replay land on
other bits while it still returnedm3_success. The replay now
applies exactly the vetoes each step recorded, as
m3World_SetPreSolveCallbackpromised; them3PreSolveFunction
comment, which said to register the callback before replaying, was
the wrong one and is corrected. Results change only for a replay run
with a callback installed, which now lands on the recorded bits. A
1.0.0 snapshot steps to the same bits, so 1.0.1 restores it.
(#363)
v1.0.0
1.0.0 (2026-11-03)
Changed
- The API is settled, and every 1.x release keeps it. A program
that builds against 1.0.0 builds against any later 1.x and means the
same: a minor release adds functions, types, enum values and fields
at the end of a struct, and never removes or renames one; a patch
release only fixes. A change that gives existing inputs a different
result ships as afeatin a minor release, or as afixin a patch
release when it corrects a result the documentation already
promised, and its changelog entry says what changes. There is no
2.0.0. The API and the results are those of 0.13.1.
(#355) - The binary interface holds within a minor version.
M3_ABI_VERSIONis now the major and minor version, so a program
built against 1.0.x headers runs on any 1.0.x library, and
m3CreateWorldrefuses a def from another minor version's headers
withm3_errorVersion. The shared library'sSOVERSIONis1.0,
andfind_package(maul3d 1.0)accepts any later 1.x.
(#355) - The C# binding keeps the same promise as the C headers.
bindings/csharp/Maul3D.csships with every release; a name it binds
is never removed or renamed in 1.x. Bind more of the API in a file of
your own, so a laterMaul3D.csreplaces this one. The Godot starter
kit stays outside the promise.
(#355) - Formats. The snapshot keeps format 6, the journal tape format 7
and the replay container format 4. They carry the new
M3_ABI_VERSION, so 1.0.0 refuses a snapshot, tape or container
that 0.13.1 or any earlier release wrote withm3_errorVersion.
From 1.0.0 the patch releases of a minor version read each other's,
except that a release that changed a result refuses a snapshot
written before it.
v0.13.1
0.13.1 (2026-11-03)
Changed
- 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
- Restore refuses a touching contact pair on a dead shape. A
shape's destroy clears the contacts of its pairs, but their keys stay
until the next step, so a snapshot taken in between holds pairs that
name a dead shape. Restore checked only that a touching pair was not
two sensors, so a snapshot could also hold a touching pair on a dead
shape. After such a restore,m3Body_GetContactDataand
m3Shape_GetContactDataread 24 bytes before the world's transforms
array, andm3World_Drawandm3Shape_SetFilterread the same
missing body. Restore now refuses such bytes withm3_errorFormat
and leaves the world as it was. Snapshots the library writes still
restore. (#350) - The contact draw skips a pair that does not touch.
m3World_DrawwithdrawContactsread the body of each pair's first
shape before checking whether the pair touched. Drawing between a
shape's destroy and the next step therefore read in front of the
transforms array for each pair whose first shape was the dead one.
Nothing was drawn from those bytes. The draw now skips a pair with no contact points first
and draws the same primitives as before.
(#350)
v0.13.0
0.13.0 (2026-11-02)
Changed
-
Breaking: five names after the final shape review. Each keeps
its value, type and meaning; rename your uses as below. Neither
binding exposes any of them.0.12.0 0.13.0 m3_voxelShapem3_voxelChunkShapem3Vehicle_GetCompressionm3Vehicle_GetWheelCompressionm3SoftBody_AnchorToSoftm3SoftBody_AnchorToSoftBodym3DrivetrainDef.autoShiftenableAutoShiftm3DrivetrainDef.diffMode,diffCoupledifferentialMode,differentialCouplingM3_MAX_DRIVETRAIN_CURVEM3_MAX_DRIVETRAIN_CURVE_POINTSM3_SERVO_REACH_LIMITM3_TARGET_REACH_LIMITM3_BLAST_LIMITM3_EXPLOSION_IMPULSE_LIMIT(#344)
-
Breaking: a dropped explosion carve has its own counter.
m3Counters.voxelEditsRefusedcounted both voxel edits refused with
m3_errorLimitand explosion carvesm3World_Explodeskipped for
the same surface bound, which no status reports. It now counts the
edits only, and the newm3Counters.voxelCarvesDroppedcounts the
skipped carves. A program that watchedvoxelEditsRefusedfor
carves readsvoxelCarvesDropped.
(#346) -
Breaking:
m3Countersdrops three fields other calls report.
scratchPeakandscratchCapacityare gone: read them from
m3World_GetMemoryUsage.snapshotBytesis gone: call
m3World_GetSnapshotSize.m3World_GetCountersno 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
- Breaking: a spherical def refuses a negative cone angle.
m3CreateSphericalJointtook a negativeconeAnglewhile
enableConewas false, a valuem3SphericalJoint_SetConeAngle
refuses with the cone on or off. The create now refuses it with
m3_errorInvalidand one misuse whether the cone is on or off, and
restore and replay refuse such bytes. Pass an angle of zero or more.
(#341)
v0.12.0
0.12.0 (2026-11-02)
Changed
-
Breaking: a def, a default and a create per joint type.
m3JointDef,m3DefaultJointDefandm3CreateJointare gone. Each
of the twelve types has its own def, default and create:
m3SphericalJointDefwithm3DefaultSphericalJointDefand
m3CreateSphericalJoint, and the same for the revolute, prismatic,
weld, distance, generic, wheel, filter, parallel, motor, gear and
pulley joints. Each def holds only the fields its type reads, so a
cone on a wheel or a spring on a weld can no longer be written.
Rename the fields you set as below. A create refuses another type's
def withm3_errorInvalidand one misuse. The default wheel def
builds: its suspension points down -y with its axle along z. The C#
JointDef,DefaultJointDefandCreateJointare replaced by the
same twelve structs, defaults and creates.0.11.0 m3JointDeffield0.12.0 typegone: call the create for your type lowerLimit,upperLimitspherical lowerTwist,upperTwist; revolutelowerAngle,upperAngle; prismatic and wheellowerTranslation,upperTranslationwheel localAxisBlocalSpinAxisBgenericLinear,genericAngularlinearModes,angularModesgenericLinearLower,genericLinearUpperlinearLower,linearUppergenericAngularLower,genericAngularUpperangularLower,angularUppergenericMotorAxismotorAxis,M3_GENERIC_NO_MOTORfor 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_errorLimitwhen they would carry the ratio past it. They change
nothing and count no misuse. Restore and replay refuse such bytes.
m3Shape_SetFilternow returnsm3Result, so check its status.
(#302) -
Breaking: body names are UTF-8 with a byte count.
m3Body_SetName(world, body, name, byteCount)returnsm3Result.
It refuses a name longer thanM3_MAX_BODY_NAME_BYTES, 31 bytes,
withm3_errorLimitwhere it cut the name, and bytes that are not
well-formed UTF-8 or hold a zero byte withm3_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_Comparereturns a status. It takes
rowsOut,capacityandtotalOutin place ofoutand
outCount, writes up tocapacityrows and sets*totalOutto 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_errorInvalidand 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; passm3DefaultQueryFilter()to keep
the old answer.m3World_OverlapCapsuletakes a center and two
points relative to it, asm3World_CastCapsuleClosestdoes, in
place of two world points.
(#297) -
Breaking: seven internal math helpers leave
core_math.h.
m3MinF,m3MaxF,m3ClampF,m3AbsF,m3MakeZeroMat3,
m3UnwindAngleandm3IntegrateRotationare no longer declared.
Use your own. The vector, matrix and quaternion math,m3ComputeCosSin
andm3Atan2stay.
(#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
- A generic joint's motor follows its switch. It drove the named
motor axis whether the motor was on or off, so
m3Joint_EnableMotor(world, joint, false)did not stop it. It now
drives only while the switch is on, like every other motor; set
enableMotorin the def to keep it driving.
(#301) - A plane sits in its body's frame everywhere. The contacts, the
fast-body sweep, the mover and the shape casts read a plane as world
space, while the ray casts, overlaps and point test placed it with
its body. A plane on a moved or turned static body collided in a
different place than queries found it. A plane on a body at the
origin collides exactly as before.
(#312) - A fast body is swept only against shapes it can touch. The
continuous phase moved a fast body back to the time it hit a shape
it never gets a contact with, so it lost most of a step's travel and
went through on the next one. It hit planes that are sensors, hidden
by its filter or on a disabled body, static surfaces its filter
hides, and shapes of a body it is jointed to with connected
collision off. The sweep now asks the same pair rule as contacts.
Scenes with such joints step to different results.
(#310,
#315) - The fast-body sweep places offset shapes. It swept every shape
as if it sat at its body's origin with no turn, so a fast body could
pass through an offset box, voxel chunk or plane, or stop in the air
above one. Bodies with offset shapes step to different results.
(#319) - Queries skip what they should.
m3World_TestPointno longer
reports shapes of disabled bodies, andm3World_OverlapAabband
m3World_OverlapSphereno longer report planes the filter hides or
planes on disabled bodies.
(#296,
#309) - A box overlap refuses a rotation that is not unit length.
m3World_OverlapBoxtook any finite quaternion and answered for a
stretched or shrunk box. A rotation whose squared length is not
between 0.98 and 1.02 is now refused as the box cast refuses it: the
call returns 0, zeroes the array and counts one misuse.
(#306) - An explosion carves only chunks its filter sees.
m3World_Explodecarved every voxel chunk in reach, including
chunks its filter hides and chunks on disabled bodies, while its
push already skipped both.
(#318)