Skip to content

Releases

v1.1.0

Choose a tag to compare

@rawframe-release rawframe-release released this 08 Nov 10:43
d5eb7e2

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 carry M3_ABI_VERSION, which now names
    1.1, so 1.1.0 refuses a snapshot, tape or container that 1.0.x wrote
    with m3_errorVersion. m3CreateWorld refuses a def from 1.0
    headers the same way: rebuild against the 1.1 headers. The shared
    library's SOVERSION is 1.1.
  • A blast inside a shape pushes its body toward its centre of mass.
    When the blast of m3World_Explode lay 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_ReplayJournal replays.
    (#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_Restore refused neither an occupied cell with
    fill 0 nor an empty cell with a fill or a payload, and
    m3World_ReplayJournal took the same states from a chunk create
    record. Both now refuse such a chunk with m3_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 with m3_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, or m3_errorCapacity when 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

Choose a tag to compare

@rawframe-release rawframe-release released this 05 Nov 06:48
c68d74a

1.0.5 (2026-11-05)

Fixed

  • M3_NODISCARD warns 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 within M3_SPEED_LIMIT along
    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_SetFill refuses an empty cell and records nothing,
    but m3World_ReplayJournal took 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 with m3_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 returns bool now 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

Choose a tag to compare

@rawframe-release rawframe-release released this 04 Nov 22:21
0518b71

1.0.4 (2026-11-04)

Fixed

  • A static body takes no velocity. A static body never moves, but
    m3CreateBody used to store a static def's velocities, and
    m3Body_SetLinearVelocity and m3Body_SetAngularVelocity took 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) with m3_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, and m3World_Restore refuses a 1.0.x snapshot in
    which a static body moves, with m3_errorFormat. Results change only
    for a host that gave a static body a velocity or called a velocity
    setter on one. A shape's surfaceVelocity is 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. Now m3Body_ApplyForce, m3Body_ApplyTorque and
    m3Body_ApplyForceAtPoint refuse 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_Restore refuses,
    with m3_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

Choose a tag to compare

@rawframe-release rawframe-release released this 04 Nov 19:27
aec1d5b

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, but m3World_Restore used 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 with m3_errorFormat and 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,
    and m3Body_SetAwake refuses any other type. m3World_Restore used
    to accept a forged or damaged snapshot with a sleeping static or
    kinematic body. It now refuses one with m3_errorFormat and 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

Choose a tag to compare

@rawframe-release rawframe-release released this 04 Nov 01:33
cf68c95

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

Choose a tag to compare

@rawframe-release rawframe-release released this 03 Nov 20:53
1fa35f5

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 returned m3_success. The replay now
    applies exactly the vetoes each step recorded, as
    m3World_SetPreSolveCallback promised; the m3PreSolveFunction
    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

Choose a tag to compare

@rawframe-release rawframe-release released this 03 Nov 11:05
42ceb21

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 a feat in a minor release, or as a fix in 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_VERSION is now the major and minor version, so a program
    built against 1.0.x headers runs on any 1.0.x library, and
    m3CreateWorld refuses a def from another minor version's headers
    with m3_errorVersion. The shared library's SOVERSION is 1.0,
    and find_package(maul3d 1.0) accepts any later 1.x.
    (#355)
  • The C# binding keeps the same promise as the C headers.
    bindings/csharp/Maul3D.cs ships 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 later Maul3D.cs replaces 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 with m3_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

Choose a tag to compare

@rawframe-release rawframe-release released this 03 Nov 01:25
b615f1a

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_GetContactData and
    m3Shape_GetContactData read 24 bytes before the world's transforms
    array, and m3World_Draw and m3Shape_SetFilter read the same
    missing body. Restore now refuses such bytes with m3_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_Draw with drawContacts read 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

Choose a tag to compare

@rawframe-release rawframe-release released this 02 Nov 22:06
53a2d08

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_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

  • Breaking: a spherical def refuses a negative cone angle.
    m3CreateSphericalJoint took a negative coneAngle while
    enableCone was false, a value m3SphericalJoint_SetConeAngle
    refuses with the cone on or off. The create now refuses it with
    m3_errorInvalid and 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

Choose a tag to compare

@rawframe-release rawframe-release released this 02 Nov 15:13
272d3ac

0.12.0 (2026-11-02)

Changed

  • Breaking: a def, a default and a create per joint type.
    m3JointDef, m3DefaultJointDef and m3CreateJoint are gone. Each
    of the twelve types has its own def, default and create:
    m3SphericalJointDef with m3DefaultSphericalJointDef and
    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 with m3_errorInvalid and one misuse. The default wheel def
    builds: its suspension points down -y with its axle along z. The C#
    JointDef, DefaultJointDef and CreateJoint are replaced by the
    same twelve structs, defaults and creates.

    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

  • 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
    enableMotor in 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_TestPoint no longer
    reports shapes of disabled bodies, and m3World_OverlapAabb and
    m3World_OverlapSphere no 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_OverlapBox took 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_Explode carved every voxel chunk in reach, including
    chunks its filter hides and chunks on disabled bodies, while its
    push already skipped both.
    (#318)