All notable changes to this project are recorded here. The format follows Keep a Changelog and the project uses Semantic Versioning. From 1.0.0, a minor release only adds to the API, a patch release only fixes, and there is no 2.0.0. A release that changes results is a minor release, and its entry says what changes. A snapshot or journal tape is read within the minor version that wrote it and refused across minor versions.
This release makes the compiler check that the library never drops the result of one of its own internal bool functions by mistake. Nothing a program sees changes: no golden line moves, and the snapshot and journal formats keep their versions.
bool carries M2_NODISCARD (#342). 231 of them lacked it, so a call that dropped such a result could go on past a refusal, or with an out value left unset, and no compiler warned. With the mark, GCC 14 found 25 calls that drop a result. Each one is meant to, so each now says so with (void):tools/check_nodiscard.py keeps every new internal bool function marked.
This release adds four calls and settings from the 1.0 API review: a motor joint's budgets change at run time, a water volume's game data changes after creation, a chain's material reads back, and each body has its own sleep threshold. It also makes M2_NODISCARD warn under GCC 14's C23. Every golden line moves, but only by value: no result moves. M2_SNAPSHOT_VERSION goes from 8 to 9 and M2_JOURNAL_VERSION from 9 to 10. As in every minor release, snapshots and tapes written by 1.2 are refused with m2_errorVersion.
ROLLBACK_SNAPSHOT hashes a snapshot's bytes, which gain the chain material arrays and the sleep thresholds. JOURNAL_TAPE hashes a tape's bytes, which carry the new journal version, the thresholds in the body create records and an embedded snapshot.| Line | 1.2.2 | 1.3.0 |
|---|---|---|
BENCH_CHAIN2000 |
2554521beecb135f |
d4b1f43baa177ee7 |
BENCH_CHAIN30 |
c9fd2db3a2d15e08 |
068decfc106c5ea2 |
BENCH_JOINTFARM |
043d06bccfc7f9b2 |
4d97fda24df98301 |
BENCH_PYRAMID15 |
6b9d8f0f43cf9b65 |
d16323c55ce611c3 |
BENCH_PYRAMID30 |
68a747f0547301f5 |
03c4be656058313a |
BENCH_WATER |
12a22f226b422370 |
49f8b8fd5d013281 |
CONTACTS |
be1739f64f0dffe3 |
6d21a40bb3c38814 |
FLUID |
8543bafd11159c27 |
89566f4410d4b11c |
JOURNAL |
47d5b04946d9c7fa |
3479f22aa69c1583 |
JOURNAL_TAPE |
404c15ec9f9ac99d |
5a657a1f11dfaf87 |
ROLLBACK |
8d7a22907589d94c |
5c83e1c8a5f16470 |
ROLLBACK_SNAPSHOT |
a8990daefb7deb76 |
9efe5ede9e675fd2 |
STACK |
ee8019b97eba97d3 |
b3238ceb722bb991 |
M2_SNAPSHOT_VERSION goes from 8 to 9 (#336): the snapshot gains each chain's friction and restitution, and each body's sleep threshold (#338). M2_JOURNAL_VERSION goes from 9 to 10 (#338): the body create record now holds the def's sleep threshold, in bytes that were zero. The record stays 88 bytes. The new records of #329 and #332 leave the journal version as it is, since the reader checks the tape's M2_ABI_VERSION before any record.m2BodyDef.sleepThreshold sets the speed below which a body counts as resting, 0 for the default 0.05 m/s, and m2Body_SetSleepThreshold changes it later. The spin limit scales with it: 0.12 rad/s times the threshold over 0.05. m2Body_GetSleepThreshold reads it back. A body at the default sleeps on the same bits as before. The def stays 80 bytes: the field takes what was its trailing padding.m2Chain_GetFriction and m2Chain_GetRestitution (#336). A chain keeps its own material, as its def or m2Chain_SetFriction and m2Chain_SetRestitution last set it, not any link's: m2Shape_SetFriction on one link does not change it. A restore checks it with the ranges the def and the setters use.m2MotorJoint_SetMaxForce, m2MotorJoint_SetMaxTorque and m2MotorJoint_GetMaxTorque (#329). A motor joint's budgets change at run time, so a platform can let go of its rider or grip harder without being made again. Each setter takes 0 to M2_FORCE_LIMIT, wakes both bodies, and counts one misuse on another joint type.m2WaterVolume_SetUserData (#332). A water volume's game data changes after creation, so the volume keeps its id.M2_NODISCARD is the attribute under GCC 14's C23 too (#334). In C it waited for __STDC_VERSION__ 202311L, but GCC 14 reports 202000L in its C23 mode. So under GCC the macro was empty, and a program could drop any status from the library without a warning. It now takes the attribute in any dialect after C17 whose compiler has it.This release makes a body whose rotation is locked hold still whatever spin the host gives it, and makes m2World_Restore refuse three more kinds of state that the step would act on wrongly. No golden line moves, and the snapshot and journal formats keep their versions. One kind of snapshot written by 1.2.0 is now refused: see the frozen pairs below.
m2Body_SetAngularVelocity gives a spin, moves differently: it no longer turns.m2World_Restore refuses one kind of snapshot that 1.2.0 wrote (#320): one taken after a static shape was moved into a sleeping body it already paired with, and the world stepped since. 1.2.0 and 1.2.1 restored it and kept the stale contact. 1.2.2 refuses it with m2_errorFormat.m2MotionLocks promises that a locked axis's velocity is zeroed each substep, but the solver zeroed only the linear axes. A locked body given a spin with m2Body_SetAngularVelocity turned at it, and nothing slowed it down: a dynamic box and a kinematic one, each given 3 rad/s, had turned 3 rad after 60 steps. The step now zeroes a locked rotation's angular velocity as it does a locked linear axis's, so it reads 0 after the step. m2Body_SetAngularVelocity still stores the spin between steps.m2World_Restore holds each frozen pair to the manifold its shapes give (#320). The step copies forward the manifold of a pair whose shapes are unmoved and whose bodies are each static or asleep for two step ends, and the touching flag decides which bodies a host call wakes. So a snapshot could say that two resting bodies do not touch. Two boxes slept in a stack, and the snapshot gave their pair 0 points: when the host teleported the lower box away, the upper one stayed asleep in the air at y 1.9991 instead of falling to y 0.9999. A restore now refuses a touching flag that disagrees with its manifold's point count, and a frozen pair whose manifold is not the one its shapes give at their bodies' poses. Only its impulses and its persisted bit may differ.m2World_Restore refuses a still body that moves (#323): a static body with a velocity, a sleeping body with a velocity, and a non-dynamic body marked asleep. A static floor with a velocity dragged the box on it like a conveyor belt, a sleeping box with a velocity shot off when woken, and a kinematic platform marked asleep stood still while m2Body_IsAwake reported it awake. A static body made with a velocity of -0, which its def accepts, still restores.m2World_Restore refuses a water volume whose box has no area (#325). m2CreateWaterVolume refuses such a box and no call changes it later, so only a snapshot could make one. The volume never acted: a box floating at y -0.2464 sank to y -6.5433. The restore and the create now put the box to the same test.This release makes a static shape that a host moves into a sleeping body touch it on the next step, and makes m2World_Restore refuse contact pairs the broadphase never makes. No golden line moves, and the snapshot and journal formats keep their versions.
m2Body_SetTransform and geometry setters such as m2Shape_SetPolygon move a static shape without waking a sleeper it does not touch, so when the two already held a pair, its old manifold stayed. A static wall teleported or widened into a sleeping box reported no contact points, no touching flag and no begin event, and moving the wall again did not wake the box. A pair with a moved shape now gets a fresh manifold, the one the same wall moved in from far away gets.m2World_Restore refuses a pair the broadphase never makes (#315): two shapes a mask or a negative group keeps apart, two sensors, two segments, a static shape with a kinematic one, two static shapes with a sensor among them, or the shapes of two bodies a joint without connected collision joins. The step kept such a pair until one of its shapes left its fat box, so the restored world pushed apart shapes the host keeps apart and reported a sensor inside another sensor. Two overlapping boxes pinned without connected collision ended 0.667 m apart after 30 steps with such a pair, instead of 0.6 m. A pair with a moved shape is still accepted, and so is a pair whose shapes drifted apart within their fat boxes.This release makes a water volume lift a body the same wherever the body is. Far from the world origin, buoyancy used to lose most of its digits: 100 km out, a floating box was thrown hundreds of meters into the air. Four golden lines move, and the snapshot and journal formats keep their versions, but as in every minor release, snapshots and tapes written by 1.1.0 are refused with m2_errorVersion.
FLUID, ROLLBACK, ROLLBACK_SNAPSHOT and JOURNAL. Each runs a water volume. The golden fluid scene floats two crates in a pool 8 to 16 m from the origin (FLUID, and ROLLBACK, whose world holds the same scene), and the seeded API walk (JOURNAL) drops its bodies into a pool reaching 14 m either side of the origin. At that distance, world-space float sums round differently from body-relative ones, so the forces change in their last bits and the worlds step to other bits. ROLLBACK_SNAPSHOT hashes the bytes of a snapshot of that world; only the values inside it differ. The other nine lines hold. A body near the origin floats as before to within rounding.| x | 1.1.0 | 1.2.0 |
|---|---|---|
| 0 | -0.122 | -0.122 |
| 10,000 | -0.049 | -0.122 |
| 100,000 | 765.8 | -0.122 |
| 10,000,000 | -19.8 | -0.122 |
This release changes how m2DecomposeOutline triangulates an outline, so the long, thin triangles its ear clipping cuts no longer drop out of the pieces. Most concave outlines get new pieces of the same area, and fewer of them. No golden line moves, and the snapshot and journal formats keep their versions, but as in every minor release, snapshots and tapes written by 1.0.1 are refused with m2_errorVersion.
m2DecomposeOutline returns new pieces for most concave outlines (#299). They cover the same area, and on average there are fewer of them. Compared bit for bit with 1.0.1, 89% of 200,000 random star-shaped outlines of 4 to 16 corners on a 0.1 m grid get new pieces, and 96% of 200,000 such outlines of 3 to 64 corners off the grid. The grid outlines average 3.13 pieces, against 3.62. An outline whose triangulation no flip improves keeps its pieces bit for bit. No golden line moves.m2DecomposeOutline keeps the thin triangles its ear clipping cuts (#299). Ear clipping takes the ear with the lowest corner index, which can cut a long, thin triangle across an outline. The polygon checks refuse a piece whose area is below 0.001 times its perimeter squared, and when no neighbor could fuse with such a triangle, the decomposition dropped it: a 10-corner outline of 16.62 square meters lost a triangle 4.9 m long holding 0.09 square meters. Between the ear clipping and the merge, the decomposition now flips an inner diagonal when the two triangles that share it form a strictly convex quad and the flip raises the worse of the two. A triangle's quality is twice its area over its longest edge squared; this is Lawson's local optimization, with that quality in place of the smallest angle. Area lost, measured:| Outlines | 1.0.1 | 1.1.0 |
|---|---|---|
| 200,000 star-shaped, 4 to 16 corners, 0.1 m grid | 201 lose area, the worst 0.5% | 0 measured |
| 20,000 star-shaped, 3 to 64 corners, off the grid | 357 lose area, the worst 0.7% | 0 measured |
The flips always end. On the hardest 64-corner outline measured, a decomposition took 325 flips and 382 µs, against 345 µs without them.
This release keeps every piece of an outline that m2DecomposeOutline splits when three of its corners lie on one straight line, as they do in a sprite outline traced on a grid. No golden line moves.
m2DecomposeOutline keeps the pieces of an outline with a straight run (#296). The merge pass fused two pieces when its convexity test, which works in double, found every corner of the union turning left. In float, a corner on a straight run can turn by a hair, so the pass fused pieces across it into one that the polygon checks refused, and the decomposition dropped it along with the valid pieces it was made of. A 6-corner outline of 17.86 square meters came back as one piece of about 5.2. The merge now fuses two pieces only when the union passes the polygon checks. Among 200,000 random star-shaped outlines of 4 to 16 corners on a 0.1 m grid, 521 lost more than 1% of their area and 27 were refused; now none loses more than 1%, and none is refused.This release settles maul2d at 1.0.0. No golden line moves, and no library behavior changes. What changes is the promise (#293): from 1.0.0, a minor release only adds to the API, a patch release only fixes, and there is no 2.0.0. A release that changes results is a minor release, and its entry says what changes. A snapshot or journal tape is read within the minor version that wrote it and refused across minor versions. The other pull requests in this release change no library code.
M2_ABI_VERSION is the major and minor version, the shared library's SOVERSION is 1.Y, and the CMake package accepts any later 1.y. Before 1.0 M2_ABI_VERSION was the whole version, and only the release that wrote a snapshot or tape read it, so 1.0.0 refuses 0.11.4's snapshots and tapes with m2_errorVersion.This release makes the world queries between steps see every shape where the last step left it. Results change: 7 of the 13 golden lines move, because the world hash covers each shape's broadphase box and moved flag. The simulation does not change: body and particle state is bit-identical, and no format version changes.
STACK, CONTACTS, ROLLBACK, JOURNAL, BENCH_CHAIN30 and BENCH_CHAIN2000 hash moving shapes' fat boxes and moved flags, and ROLLBACK_SNAPSHOT's bytes carry the trees and the moved flags, which the end-of-step refit now sets. FLUID, JOURNAL_TAPE, BENCH_PYRAMID15, BENCH_PYRAMID30, BENCH_JOINTFARM and BENCH_WATER hold. test_golden compiled against both versions with m2World_Hash replaced by a hash of only every body's transform and velocities and every particle's position and velocity agrees on 12 of the 13 lines, the seeded API walk and every per-step mix included; the 13th is ROLLBACK_SNAPSHOT, which hashes snapshot bytes. The snapshot and the tape keep their layout, and only the box values and moved flags inside a snapshot differ.| Comparison | Before | After |
|---|---|---|
m2World_OverlapAabb against a brute-force test of every shape's m2Shape_GetAabb, 9800 random queries over a world of 60 boxes on static, kinematic and dynamic bodies moving at up to 40 m/s per axis, every seventh teleported every five steps, over 40 steps |
165 leave a shape out | 0 |
| A ray through the center of a ball falling at 20 m/s, after each of 30 steps | misses after 24 | 0 |
m2World_CastRayAll against m2Shape_CastRay on every shape, 9800 random rays in the same world |
237 miss a shape | 0 |
This release stops a bullet from passing a wall when more than 64 shapes crowd its path, and makes m2World_CollideMover with a short array hold the planes of the lowest shape slots, whatever order the broadphase trees yield them. No golden line moves.
m2World_CollideMover with an array shorter than the planes it finds holds the planes of the lowest shape slots (#273). It kept the planes the broadphase trees yielded first, then sorted only those. The trees' order follows their history, which the world hash does not cover, so two worlds with the same hash could hand a controller different planes: twelve posts around a capsule, with room for three, kept shapes 1, 2, 3 in one tree history and 1, 3, 4 in another. A short array now holds the planes of the lowest shape slots, in ascending order; the returned total is unchanged, and an array with room for every plane gets the same planes in the same order as before. Its /// block and the guide's events and queries page say so.This release keeps the contacts of a particle near more than 32 shapes, and speeds up a spring fill. No golden line moves.
m2CreateParticlesInPolygon with m2_springParticle tested every pair of the particles it made, so its time grew with the square of the batch. A fill emits its lattice row by row from the bottom, so for each particle the scan now covers the rest of its row and the row above, fewer than 512 particles. An 80000-particle fill took 2.7 s and now takes 0.011 s (RelWithDebInfo, clang-20). The springs come out the same, in the same order, and the same fills reach the same m2World_Hash before and after.m2Counters.particleBodyContactsDropped did not count it, and when the particle touched the crowd the counter never passed 28. A particle now tests every non-sensor shape whose box overlaps its own and keeps the four touching shapes with the lowest slots; every touch past four counts as a drop. The kept set depends only on which shapes touch, not on the order the broadphase trees yield them. When no more than 32 shapes overlap a particle's box, the kept contacts are the same as before, in the same order.This release stops two shapes without area from colliding: a segment or a chain segment never pairs with another, as in Box2D. Such a pair could not hold a body anyway: it touched for a step before the body sank through. One golden line moves, and the snapshot and journal formats stay as they were.
M2_JOURNAL_HASH, the same in the scalar and AVX2 builds (#265). The seeded API walk behind it attaches chains to random bodies, dynamic ones included. Their pairs with other segments and chains are no longer made, so the walk's worlds step to other bits. No other golden scene makes such a pair, and their lines hold. The snapshot (version 8) and journal tape (version 9) formats are unchanged.m2Segment, m2ChainSegment and the guide's bodies and shapes page say so.This release makes a rollback keep the end events a destroy or a disable queued between steps. To carry them, the snapshot and journal formats change, so 0.11.0 refuses 0.10.0's snapshots and tapes. No simulation result changes.
m2_errorVersion.M2_ROLLBACK_SNAPSHOT_HASH and M2_JOURNAL_TAPE_HASH. Every simulation line stays, so the same calls step to the same bits as on 0.10.0.m2_errorFormat.This release makes two kinds of argument refusals stricter, keeps a refused journal replay from changing anything a program can see, keeps host code from destroying the world it runs in, and stops m2ComputeHull from shrinking fine curves. A program that relied on the two old leniencies below needs to change. No golden value moves, so the same calls step to the same bits as on 0.9.0.
| Call | A negative capacity, or a null array with a capacity above 0 |
|---|---|
The 19 list reads (the id lists, m2Shape_GetSensorOverlaps, m2World_GetContactData, the overlap and cast-all queries, m2World_CollideMover, m2World_OverlapParticlesAabb) |
return 0 and count one misuse, instead of measuring |
m2DecomposeOutline |
m2_errorInvalid, instead of succeeding |
m2World_Snapshot and m2World_StartJournal (a negative capacity) |
m2_errorInvalid with one misuse, instead of m2_errorCapacity |
A program that measured with NULL and a capacity other than 0 passes 0. m2World_Restore and m2World_ReplayJournal still answer a negative size with m2_errorFormat and no misuse, as for any bytes that are not a snapshot or tape.
- Breaking: m2CreateWorld checks the particle settings (radius, density, gravity scale and the eight strengths) even when particleCapacity is 0 (#233). A def with a setting outside its range, NaN included, is refused in a world without particles too. Such a world used to be created and then refuse its own snapshots and tapes with m2_errorFormat.
- Results and formats: no golden value moves. The snapshot (version 7) and journal tape (version 8) formats are unchanged. As before 1.0.0, 0.10.0 reads only its own snapshots and tapes, and refuses 0.9.0's with m2_errorVersion.
m2ComputeHull keeps every merged point within 0.01 m of the edge that replaces it (#229). On a dense curve, such as a finely traced sprite outline, the merge used to eat the curve and return a smaller hull with m2_success: 256 points on a unit circle gave an octagon with points up to 0.09 m outside it. A curve that needs more than M2_MAX_POLYGON_VERTICES vertices to stay that close is now m2_errorLimit, as the header says for any hull of more vertices. A hull the old merge built correctly comes out the same.m2World_ReplayJournal leaves the event streams as they were (#236). It used to empty the last step's events and drop an end event that a destroy between steps had queued, so a host saw a contact begin and never end.m2World_ReplayJournal leaves the counters as they were, except that misuse by host code the replay ran stays counted (#238). It used to leave the last-step counts of the steps it ran before it failed. The m2Counters header and docs/user/diagnostics.md now say which counters a restore leaves: the last-step counts and the cumulative counts are not snapshotted, so right after a rollback the last-step counts still describe the step before it.m2DestroyWorld called from host code the world runs (a task hook, a debug draw callback, the allocator) is refused, as every other writer is (#243). It counts one misuse and the world stays. It used to free the world while the world was still using it, and from a free hook during a destroy it freed the world twice.This release settles the API ahead of 1.0.0. The step and the target transform now return a status. The diagnostics and the water volume take maul3d's names, and the public kinds have fixed sizes. Every call clears its outputs the same way when it fails. A program needs the changes below to build again. No golden value moves, so the same calls step to the same bits as on 0.8.0.
m2World_Step returns m2Result and is marked M2_NODISCARD (#221). A refused step still changes nothing and counts one misuse, as before; a program now sees the refusal in the status:| Input | Status |
|---|---|
| a null world | m2_errorInvalid, no misuse |
dt NaN, 0 or below |
m2_errorInvalid |
a positive dt outside M2_MIN_STEP to M2_STEP_LIMIT, infinity included |
m2_errorLimit |
| a substep count below 1 | m2_errorInvalid |
a substep count above M2_MAX_SUBSTEPS |
m2_errorLimit |
| a call from host code the world is running | m2_errorBusy |
Check the status; a refused step leaves the last step's contact events in place.
- Breaking: m2Body_SetTargetTransform returns m2Result and is marked M2_NODISCARD (#195). It returns m2_errorStale for a stale id and m2_errorInvalid for a static body or a bad pose. For a dt outside M2_MIN_STEP to M2_STEP_LIMIT, it returns m2_errorLimit and counts a misuse. For a pose too far to reach within M2_SPEED_LIMIT in one step, it returns m2_errorLimit and counts no misuse, since a correct program can meet that.
- Breaking: the m2Counters and m2Profile fields take the family's names, as in maul3d (#203). A count ends in Count, work lost without telling the caller in Dropped, and calls refused with a status in Refused:
| Old name | New name |
|---|---|
bodies |
bodyCount |
awakeBodies |
awakeCount |
shapes |
shapeCount |
joints |
jointCount |
pairs |
contactCount |
touchingPairs |
touchingCount |
constraints |
constraintCount |
graphColors |
colorCount |
overflowConstraints |
overflowCount |
pairOverflow |
pairsDropped |
particlePairOverflow |
particlePairsDropped |
particleBodyOverflow |
particleBodyContactsDropped |
particlePoolFull |
particleCreatesRefused |
misuse |
misuseCount |
m2Profile.stepMs |
m2Profile.step |
m2Profile.pairsMs |
m2Profile.broadphase |
m2Profile.contactsMs |
m2Profile.narrowphase |
m2Profile.solveMs |
m2Profile.solve |
m2Profile.sleepMs |
m2Profile.sleep |
stepCount keeps its name, and the profile is still in milliseconds. awakeCount now counts only enabled, awake dynamic bodies. awakeBodies also counted static, kinematic and disabled bodies, which never sleep.
- Breaking: the buoyancy volume is a water volume, as in maul3d, so its name no longer suggests the particle fluids (#199). Every m2FluidVolume name is now m2WaterVolume: the id, m2_nullWaterVolumeId, the def, m2DefaultWaterVolumeDef, the create and destroy calls and m2WaterVolume_*. m2WorldDef.fluidVolumeCapacity is waterVolumeCapacity. m2WaterVolumeDef takes its box as bounds, an m2WorldAabb, in place of regionLower and regionUpper. m2WorldAabb is now declared in core_math.h.
- Breaking: m2CreateWaterVolume refuses a box without area, with m2_errorInvalid and one misuse (#201). A box with area has its upper corner above the lower on both axes; a volume without one could never act. The default def's box is one point at the origin, so set bounds before creating.
- Breaking: m2BodyType, m2JointType and m2ShapeType are int32_t typedefs, and m2ParticleFlags is a uint32_t typedef, each with its constants in an anonymous enum (#184). Their sizes no longer depend on the compiler. Code that names enum m2BodyType and the like uses the typedef. The constants keep their names and values. m2ParticleDef.flags is an m2ParticleFlags, and m2Particle_GetFlags returns one. m2IsCpuBackendSupported returns bool, and m2Hash64 takes an int64_t byte count.
- Breaking: a call that fails clears its outputs, by the family rule:
- Refused list reads zero the caller's array over its whole capacity, instead of leaving it as it was (#183). These are the id lists, m2Shape_GetSensorOverlaps, m2World_GetContactData, the overlap and cast-all queries, m2World_CollideMover and m2World_OverlapParticlesAabb. A read that succeeds still writes only the items it reports.
- A failed m2World_Snapshot zeroes its buffer over its capacity, m2_errorCapacity included (#183). A ring of snapshot buffers sizes each one with m2World_GetSnapshotSize first, or snapshots into a spare and swaps it in on success.
- The closest casts (m2World_CastRayClosest, m2World_CastCircleClosest, m2World_CastCapsuleClosest, m2World_CastPolygonClosest) and m2Shape_CastRay zero hitOut, with the null shape id, on a miss and on a refusal (#166). A program that kept an earlier hit in hitOut across a miss keeps its own copy.
- A call that the world's own host code makes, and that is refused with m2_errorBusy, leaves its count as it was. This covers m2Body_Shatter's countOut (#173), m2World_StopJournal's bytesOut (#178) and m2CreateParticlesInPolygon's countOut (#179). Such a call with a null count returns m2_errorBusy instead of m2_errorInvalid.
- m2Body_Shatter fills bodiesOut with null ids on every failure, a null countOut included (#173).
- Breaking: m2Shape_GetSensorOverlaps on a live shape that is not a sensor counts one misuse and zeroes the array over its capacity, as for a stale id (#225).
- m2World_OverlapParticlesAabb refuses an inverted box, with one misuse and a zeroed list, as m2World_OverlapAabb already did (#201). A flat or one-point box is still a fair query.
- Results and formats: the same calls step to the same bits as on 0.8.0, and no golden value moves. The snapshot (version 7) and journal tape (version 8) formats are unchanged. As before 1.0.0, 0.9.0 reads only its own snapshots and tapes, and refuses 0.8.0's with m2_errorVersion.
m2MinF, m2MaxF, m2AbsF, m2ClampF and m2UnwindAngle are no longer in the public headers (#197). A program uses its own min, max and abs. m2MakeRot still maps any angle into [-pi, pi]. core_math.h no longer pulls <xmmintrin.h> or <arm_neon.h> into a program built with MSVC.m2DestroyWorld no longer crashes when the allocator's free hook reads the world back (#209). The world is emptied before its first pool is freed, so the hook finds every id stale and every list empty.m2World_ReplayJournal refuses a tape with m2_errorFormat when one of its void calls is refused (#217). Before, it skipped the call and returned m2_success. Examples are a velocity edited to NaN and a step edited to a dt of 0. No tape a recording wrote is affected.m2CreateParticle and m2CreateParticlesInPolygon refuse particle flags with a bit the library does not define, with m2_errorInvalid and one misuse (#215). A restore refuses undefined bits in particle flags, joint flags and motion locks with m2_errorFormat.m2CreateMouseJoint counts no misuse when it refuses a target too far from body B, and returns m2_errorLimit at any rotation of B (#195). Before, a target past the float range with B turned exactly a quarter returned m2_errorInvalid; ordinary targets were never affected.m2Body_GetLocalPointVelocity counts one misuse for a stale id instead of two (#192).m2CreateWorld refuses, with m2_errorCapacity and before allocating anything, a jointCapacity above 1,073,741,823. Above that, the joint edge count wrapped 32 bits (#187).m2World_Hash covers a pool's generations in a world with more than 536,870,911 slots in that pool (#184). Above that, the pool's byte count overflowed 32 bits and its generations dropped out of the hash.INT32_MAX instead of overflowing (#189). Worlds with more than 2^30 bodies or water volumes no longer overflow in the hash, the free rings or the restore check.This release renames the API by the Maul family's naming rule, which maul3d follows too. Every change is a rename, so a program builds again once it uses the new names below, and nothing it computes changes.
| Old name | New name |
|---|---|
m2World_ShatterBody |
m2Body_Shatter |
m2World_SnapshotSize |
m2World_GetSnapshotSize |
m2TaskFn |
m2TaskFunction |
m2EnqueueTaskFn |
m2EnqueueTaskFunction |
m2FinishTaskFn |
m2FinishTaskFunction |
m2ResultName |
m2GetResultName |
m2CpuSupportsBackend |
m2IsCpuBackendSupported |
The m2WorldDef fields enqueueTask and finishTask keep their names.
- Breaking: the body switches are named as in maul3d (#159). A body switched on and off as a whole is m2Body_Enable and m2Body_IsEnabled, and a bullet is a part switched on and off, m2Body_EnableBullet and m2Body_IsBulletEnabled:
| Old name | New name |
|---|---|
m2Body_SetEnabled |
m2Body_Enable |
m2BodyDef.isEnabled |
m2BodyDef.enable |
m2Body_SetBullet |
m2Body_EnableBullet |
m2Body_IsBullet |
m2Body_IsBulletEnabled |
m2BodyDef.isBullet |
m2BodyDef.enableBullet |
m2Body_IsEnabled keeps its name.
- Breaking: the shape geometry getters each read one shape type and refuse every other, so they name it (#162):
| Old name | New name |
|---|---|
m2Shape_GetCircle |
m2CircleShape_GetCircle |
m2Shape_GetCapsule |
m2CapsuleShape_GetCapsule |
m2Shape_GetPolygon |
m2PolygonShape_GetPolygon |
m2Shape_GetSegment |
m2SegmentShape_GetSegment |
m2Shape_GetChainSegment |
m2ChainSegmentShape_GetChainSegment |
m2Shape_SetCircle and the other geometry setters keep their names: they take a shape of any type and change it.
- The arrays the list calls fill are named with Out in the headers (idsOut, hitsOut, planesOut, overlapsOut, dataOut, piecesOut, and bufferOut for m2World_Snapshot). No call changes.
- Results and formats: the same calls, renamed, step to the same bits as on 0.7.0, and no golden value moves. The snapshot (version 7) and journal tape (version 8) formats are unchanged, but as before 1.0.0, 0.8.0 reads only its own snapshots and tapes and refuses 0.7.0's with m2_errorVersion.
This release ends the API audit before 1.0.0. Every change is a rename or a new parameter order, so a program builds again once it follows the items below, and nothing it computes changes.
m2World_CastRayAll, m2World_CastCircleAll, m2World_CastCapsuleAll, m2World_CastPolygonAll, m2World_OverlapCircle, m2World_OverlapCapsule, m2World_OverlapPolygon, m2World_OverlapAabb and m2World_CollideMover take (world, ..., filter, out, capacity). The filter used to come last.m2QueryFilter argument from the end to just before the output array. A call left in the old order no longer compiles.| Old name | New name |
|---|---|
M2_MOVER_PLANES |
M2_MAX_MOVER_PLANES |
m2RatchetJointDef.ratchet |
m2RatchetJointDef.toothAngle |
m2RatchetJoint_GetRatchet |
m2RatchetJoint_GetToothAngle |
maxMotorTorque (revolute and wheel defs) |
maxMotorEffort |
maxMotorForce (prismatic def) |
maxMotorEffort |
m2Joint_SetMaxMotor |
m2Joint_SetMaxMotorEffort |
m2Joint_GetMaxMotor |
m2Joint_GetMaxMotorEffort |
A motor's maxMotorEffort is still a torque on revolute and wheel joints and a force on prismatic joints.
- Results and formats: the same calls, renamed and reordered, step to the same bits as on 0.6.0, and no golden value moves. The snapshot (version 7) and journal tape (version 8) formats are unchanged, but as before 1.0.0, 0.7.0 reads only its own snapshots and tapes and refuses 0.6.0's with m2_errorVersion.
This release settles most of the API before 1.0.0, so nearly every program needs changes to build against it. Each Breaking item says what to change.
m2_errorFormat (-8), m2_errorLimit (-9) and m2_errorBusy (-10), named by m2ResultName (#133).M2_MAX_* is a count, M2_*_LIMIT a magnitude and M2_MIN_* a floor. New ones: M2_MAX_OUTLINE_POINTS and M2_MAX_SHATTER_PIECES (64 each), M2_MAX_HULL_POINTS (256), M2_MAX_FILL_COLUMNS (256) and M2_MIN_PARTICLE_RADIUS (0.02 m) (#133, #141).m2ParticleDef and m2DefaultParticleDef (#109).m2Joint_EnableConnectedCollision changes a live joint's connected collision;m2Joint_EnableSpring turns a distance, wheel or revolute spring on or off;m2Chain_IsLoop reads a chain's kind;m2DistanceJoint_GetLengthRange reads what m2DistanceJoint_SetLengthRange sets;m2Joint_SetConstraintHertz and m2Joint_SetConstraintDampingRatio, with their getters, soften the rows that hold a revolute, prismatic, gear, pulley or ratchet joint together;m2WeldJoint_SetLinearSpringHertz, SetLinearSpringDampingRatio, SetAngularSpringHertz and SetAngularSpringDampingRatio, with their getters, tune a weld's two springs;m2_errorBusy, not m2_errorState;m2_errorFormat, not m2_errorInvalid, and count no misuse;m2_errorLimit, not m2_errorInvalid;m2_errorInvalid.
Code that compares a status with m2_errorInvalid or m2_errorState needs the new cases.| Old name | New name |
|---|---|
M2_HUGE |
M2_LOCAL_LIMIT |
M2_MAX_SPEED |
M2_SPEED_LIMIT |
M2_MAX_ACCELERATION |
M2_ACCEL_LIMIT |
M2_MAX_GRAVITY_SCALE |
M2_GRAVITY_SCALE_LIMIT |
M2_MAX_RATE |
M2_RATE_LIMIT |
M2_MAX_COEFFICIENT |
M2_COEFFICIENT_LIMIT |
M2_MAX_DENSITY |
M2_DENSITY_LIMIT |
M2_MAX_RATIO |
M2_RATIO_LIMIT |
M2_MAX_MASS |
M2_MASS_LIMIT |
M2_MAX_INERTIA |
M2_INERTIA_LIMIT |
M2_MAX_FORCE |
M2_FORCE_LIMIT |
M2_MIN_TIME_STEP |
M2_MIN_STEP |
M2_MAX_TIME_STEP |
M2_STEP_LIMIT |
M2_MAX_ANGLE |
M2_ANGLE_LIMIT |
M2_JOINT_REACH |
M2_REACH_LIMIT |
| - Breaking: joints use one spring and constraint vocabulary (#123). | |
| - Rename the def fields: | |
- hertz and dampingRatio become springHertz and springDampingRatio on distance, wheel, motor and mouse joints; |
|
- they become constraintHertz and constraintDampingRatio on revolute and prismatic joints; |
|
- weld linearHertz, linearDampingRatio, angularHertz and angularDampingRatio become linearSpringHertz, linearSpringDampingRatio, angularSpringHertz and angularSpringDampingRatio. |
|
- A distance joint is a spring only with enableSpring on, and is a rigid rod with it off. A revolute joint's angular spring now needs the def's new enableSpring. Turn enableSpring on wherever a spring used to be on through its hertz alone. |
|
- m2Joint_SetSpringHertz and m2Joint_SetSpringDampingRatio set a revolute joint's spring, not its pin, and refuse prismatic, weld, gear, pulley and ratchet joints. Use m2Joint_SetConstraintHertz and m2Joint_SetConstraintDampingRatio for the pin. |
|
| - Breaking: one cast hit type (#121). | |
- m2CastHit replaces m2RayCastResult and m2RayHit, and has no hit flag. |
|
- m2World_CastRayClosest, m2Shape_CastRay, m2World_CastCircleClosest, m2World_CastCapsuleClosest and m2World_CastPolygonClosest return bool and write the hit through a new last parameter, m2CastHit* hitOut. A miss leaves hitOut as it was, and a null hitOut counts one misuse. |
|
- The casts that list every hit fill m2CastHit arrays. |
|
- Breaking: sensor events have their own types, m2SensorBeginEvent and m2SensorEndEvent: {sensorShapeId, visitorShapeId, step} (#125). |
|
| - Read the sensor and the visitor directly, instead of testing both shapes of a contact pair. | |
| - A sensor begin no longer carries a normal, contact points or an approach speed. | |
- Breaking: snapshot and journal sizes are int64_t (#127). |
|
- m2World_SnapshotSize and m2World_GetJournalBaseSize return it. |
|
- m2World_Snapshot, m2World_Restore, m2World_StartJournal, m2World_StopJournal and m2World_ReplayJournal take int64_t sizes and int64_t* outputs. |
|
- M2_MAX_SNAPSHOT_BYTES is gone, and m2CreateWorld no longer refuses a large world for its snapshot size. The one exception is a 32-bit target where a size_t cannot hold the size. |
|
| - Breaking: particles are created like other objects (#109). | |
- m2CreateParticle replaces m2World_EmitParticle. |
|
- m2CreateParticlesInPolygon replaces m2World_FillPolygonWithParticles. It returns m2Result and reports its count through countOut. |
|
- A fill the pool cuts short keeps what it made and returns m2_errorCapacity. |
|
| - Breaking: names follow one rule (#114, #119, #98, #100): | |
- m2WorldDef.enableSleeping becomes enableSleep, with m2World_EnableSleep and m2World_IsSleepEnabled; |
|
- every joint def's collideConnected becomes enableConnectedCollision, and m2Joint_GetCollideConnected becomes m2Joint_IsConnectedCollisionEnabled; |
|
- m2Body_SetEnabled(world, body, bool) replaces m2Body_Enable and m2Body_Disable; |
|
- m2AabbResult becomes m2WorldAabb, and m2World_OverlapAabb and m2World_OverlapParticlesAabb take one m2WorldAabb instead of two corners; |
|
- m2Joint_GetLength becomes m2DistanceJoint_GetLength, and m2Joint_GetLimits no longer reads a distance joint; |
|
- m2Body_GetRotationalInertia becomes m2Body_GetCentralInertia, and m2Body_GetMass moves to maul2d/body.h. |
|
| - Breaking: a pool sized 0 is full from the start (#143, #117). | |
- m2CreateParticle, m2CreateParticlesInPolygon and m2CreateFluidVolume return m2_errorCapacity and count no misuse in a world whose pool for them is sized 0. They used to return m2_errorInvalid. |
|
- m2CreateChain returns m2_errorCapacity when the shape pool cuts a chain short. chainOut still names the shorter chain. |
|
- Breaking: m2ComputeHull hulls the whole cloud, up to M2_MAX_HULL_POINTS points. It returns m2_errorLimit for a larger cloud, or for one whose hull has more than M2_MAX_POLYGON_VERTICES vertices (#141). |
|
- Breaking: both formats change, so 0.6.0 refuses the snapshots and tapes of 0.5.0 with m2_errorVersion. |
|
| - Snapshot version 7. The header holds the world's particle radius, and a snapshot restores only into a world with the same capacities and particle radius (#105). Each chain's loop flag is part of the state (#114). | |
| - Journal tape version 8. The header is 24 bytes and a restore record's size is 64 bits (#127). The body and revolute records follow their new defs (#100, #123). | |
- Results: a program that makes the same calls, with the renames above and enableSpring turned on wherever a spring was on, steps its bodies, contacts and particles to the same bits as on 0.5.0. Only the fixes below change results. m2World_Hash covers each joint's spring switch, so a world whose joints now turn enableSpring on hashes differently from 0.5.0, even though it steps the same. |
m2BodyDef.fixedRotation, m2Body_SetFixedRotation and m2Body_IsFixedRotation: lock rotation with m2MotionLocks.angularZ (#100);m2Joint_GetHertz, m2Joint_GetDampingRatio, m2Joint_GetAngularHertz and m2Joint_GetAngularDampingRatio: use the m2Joint_GetSpring* calls (#98);m2Joint_SetAngularSpringHertz, m2Joint_SetAngularSpringDampingRatio and their getters: use m2Joint_SetSpringHertz on a revolute and m2WeldJoint_SetAngularSpringHertz on a weld (#123);m2DistanceJoint_IsSpringEnabled: use m2Joint_IsSpringEnabled (#114).m2ComputeHull no longer ignores every point after the eighth (#141).M2_REACH_LIMIT stays finite, by treating the ends as that far apart. Before, it made the world's particle state non-finite. Nearer links keep their results (#105).m2_errorFormat, before it changes anything (#105).Breaking: m2CreateWorld refuses with m2_errorCapacity a definition whose world would snapshot to more than M2_MAX_SNAPSHOT_BYTES (1,073,741,801 bytes, about 840,000 shape slots), before it allocates anything. Every world within that size snapshots, restores and records a journal (#92).
No simulation result changes. The snapshot (version 6) and journal tape (version 7) formats are unchanged; as before 1.0.0, 0.5.0 reads only its own snapshots and tapes and refuses 0.4.0's with m2_errorVersion.
m2Shape_GetSensorOverlaps and m2World_GetContactData measure with a null array at any capacity, as documented; a null array with a capacity above 0 used to crash (#95).M2_ABI_VERSION, the format's own
version and the header's size. Bytes from another release, another
format version or another build are refused with m2_errorVersion
instead of m2_errorInvalid, and so is a tape that starts with another
release's snapshot. Before 1.0.0 every release is its own ABI version
(#65).M2SN. A
snapshot written by an earlier release is refused
(#65,
#76).M2JN in a hex dump, and its
set-mass-data record carries the inertia about the center of mass. A
tape written by an earlier release is refused
(#65,
#76,
#80).m2MassData takes the inertia about the center of mass.
centralInertia replaces rotationalInertia, which was the inertia
about the body origin, in m2Body_SetMassData and
m2Body_GetMassData. Pass the inertia about the centroid, as most
formulas give it (a disc's is m r^2 / 2), and drop any parallel-axis
shift. A small inertia beside a far center is now kept as given: a
1 kg body with its center 1 m from its origin and a central inertia of
1e-10 used to read back as unable to turn
(#80).userData, the filter bits, the event steps, the counters) is 8-byte
aligned through M2_ALIGN8, so a program built against the old
headers on 32-bit x86 must be rebuilt. Other targets keep their layout
(#78).m2World_Restore refuses a snapshot whose particle springs or triads
join a dead particle, where it accepted it and left a world that
m2World_Validate refuses
(#73).m2DecomposeOutline refuses with m2_errorInvalid
an outline whose pieces all fail validation, such as a triangle
100 km long and 1 cm tall, where it returned m2_success with a
count of 0. In both the fill and the measure call the count is left
as it was, as for every refusal
(#53).m2DecomposeOutline refuses a clockwise outline far from the
origin. Its winding test summed the area in float, so near 98304 m a
small clockwise hexagon summed positive and split into two pieces.
The area is now summed in double
(#53).m2MakePolygon refused every 0.25 m to 2 m box and right triangle at
65536 m and 98304 m, most of them at 10000 m, and one at 1000 m.
m2ComputeHull, the pieces of m2DecomposeOutline, the shape
constructors, m2Shape_SetPolygon, m2World_ShatterBody and
restore all go through the same check. The area is now summed in
double, against the same threshold
(#53).m2Body_SetAwake(false),
refreshes its contacts. It kept its old sleep streak, so its pairs
kept the manifold from before the move: in the reproduction the
contact points sat 0.4 m behind the box. The streak now starts from
zero wherever a body is put to sleep
(#51).m2Shape_SetCircle,
m2Shape_SetCapsule, m2Shape_SetPolygon, m2Shape_SetSegment)
collides over its new extent. Its broadphase box used to keep the old
extent, so a ball dropped over the new part of a widened static box
fell through it. On a dynamic body the box is refitted one step
earlier than before, with the same result. The world hash differs
from 0.1.0's after such a change, since the box is part of it
(#48).m2Body_SetAwake(false). The world hash can therefore differ from
0.1.0's when a body falls asleep past its box margin; in no golden
scene does one
(#49).m2World is an opaque pointer,
and m2CreateWorld(def, &worldOut) returns an m2Result. Every call
on a world's objects takes the world first, and readers take
const m2World*. Ids are {index1, generation}, with no world field.
Creates, destroys, snapshots, restores, the journal calls and the
polygon builders return an m2Result and write through Out
parameters. The world table, m2WorldId, m2World_IsValid, the
*_GetWorld getters, m2SetAllocator, m2SetAssertHandler,
m2LastResult, m2_errorConfig and m2WorldDef.workerCount are
removed. The allocator is part of m2WorldDef, which begins with its
cookie and an ABI version that m2CreateWorld checks
(m2_errorVersion). Worlds share nothing, so separate worlds may be
created, stepped and destroyed on separate threads.
(#12)m2Counters.misuse counts the calling
code's bugs: an invalid argument, a stale id, a writer called from
host code. A stale id counts one misuse in every call, while a stale
particle id counts nothing, since particles die on their own. Corrupt
snapshot or tape bytes count nothing. Host code a replay runs (the
task hooks and the allocator) counts like any other caller.
Motor and limit calls on a joint without a motor or limit change
nothing and count one misuse; for a distance joint, use
m2DistanceJoint_SetLengthRange.
(#13,
#14,
#15)base.h, each listed in docs/user/limits.md: speeds up to
1e5 m/s, masses from 1e-9 to 1e12 kg, time steps from 1e-6 to 10 s,
M2_JOINT_REACH of 1e9 m, and the rest. These are the other refusals:M2_HUGE (1e5 m) in shapes, chains,
outlines and joint defs.M2_MAX_SUBSTEPS (64) in m2World_Step.m2Body_SetMassData.m2DecomposeOutline refuses an outline that touches
itself, or one whose vertex comes within 0.005 m of an edge it does
not end.(#17,
#22,
#23,
#24,
#25,
#26,
#32,
#34,
#40)
- Breaking: what a restore or replay accepts. m2World_Restore
refuses a snapshot in any of these cases, and leaves the world as it
was:
- its arrays do not fit together (free queues, shape lists, owners,
broadphase trees);
- it holds shape geometry the world would not build;
- its mass data is not what the body's shapes give it;
- a body rotation in it is not of unit length.
Replay refuses a tape record whose bool flags are not 0 or 1.
Snapshots are format 3 and journal tapes format 4, so those written
by 0.0.1 are refused.
(#23,
#27,
#29,
#31,
#39)
- Breaking: the world hash. m2World_Hash and
m2World_GetHashParts now also cover:
- every setting the step reads;
- the live slots, keyed by index;
- the id pools;
- the broadphase's fat boxes.
So two worlds with the same hash step on to the same bits. Every
hash value changes, and m2WorldHashParts gains a volumes part.
Queries capped at a number of hits keep the lowest shape slots, so
they choose the same shapes in every world that hashes the same.
(#37,
#38)
- Breaking: m2World_ShatterBody returns an m2Result and hands
the count back through countOut, and m2IsNormalizedRot returns
bool.
(#19,
#20)
m2DistanceJoint_IsSpringEnabled reads a distance joint's spring
toggle back.
(#21)M2_MAX_SPEED, and every joint and contact impulse at
M2_IMPULSE_LIMIT, and water and wind loads at M2_MAX_FORCE. A 2x2
joint solve whose couplings would overflow solves its rows one at a
time instead.
(#42)M2_JOINT_REACH, where it used to leave NaN in the
bodies.
(#35){0, 0}, NaN or the identity.
(#23)The first release of the reworked library.
m2Counters.pairOverflow: body pairs dropped by the last pair
update because the pair table was full. Dropping pairs used to be a
debug-only assert.bench/: benchmark scenes that time Maul2D and pin their final
world hashes in bench/pins.txt. CI fails when a pin moves.MAUL2D_BUILD_SHARED builds a shared library. Public functions
carry the M2_API export macro and everything else is hidden.test/hashes.txt holds the expected determinism hashes, and
tools/check_hashes.py compares a test run with them, so a change
that moves a hash the same way on every platform is caught too.M2_ASSERT is kept for internal invariants; every refusal instead
records its reason for m2LastResult and, for a stale or wrong-kind
id, counts in m2Counters.misuse.MAUL2D_BUILD_TESTS,
_BUILD_SAMPLES, _BUILD_BENCH, _BUILD_TOOLS, _BUILD_TESTBED,
_BUILD_SHARED, _INSTALL, _WERROR, _SANITIZE, _TSAN and
_COVERAGE. Tests, samples, benchmarks and tools default to on only
when the project is built on its own, not when it is added with
add_subdirectory.MAUL2D_WERROR=ON (CI turns it on),
so a newer compiler no longer breaks a consumer's build.cmake/, the package config template and the
pkg-config template are shared with the sibling engine. The pkg-
config file is relocatable and no longer lists a thread library the
engine does not use.test/test_harness.h instead of
its own copy of the check macro.test/hashes.txt, and clang-tidy, the package consumer and a
shared-library build are checked on every push. Releases ship Linux
(x64 and arm64), macOS and Windows packages; the portable x64
package is gone, build with -DMAUL2D_SIMD=scalar instead. The
documentation site now carries the guide, the API reference and the
changelog, and the browser testbed moves to /testbed/.maul2d/math.h header is now maul2d/core_math.h, so it
can never shadow the C library's math.h when include/maul2d
lands on an include path.src/world_state.c. Allocation, release, the snapshot walk and the
memory footprint all read that table, so adding an array touches the
struct and one table row instead of seven places.
m2World_MemoryBytes now also counts the broadphase trees and the
solver scratch.m2SetLastResult from the public header. It was internal.MAUL2D_BENCHMARKS option, along
with the performance comparison in the README that relied on them.m2AssertFail and the M2_ASSERT macro from the public header.
They are internal; hosts keep m2SetAssertHandler.m2Body_GetJoints and body destruction no longer scan all joints.m2World_CastRayAll and m2World_CastCircleAll silently kept at
most 64 hits even when the caller's array was larger. They now keep
as many as the array holds.m2World_Restore refuses a snapshot whose header counters point
outside the world's capacities before overwriting anything, instead
of indexing past the arrays afterwards.m2LastResult was set by only a handful of refusals and lived in
one process-wide slot. Every refusal now records its reason (invalid
input, full pool or allocation failure, unsupported CPU) and the
slot is per thread.tools/gen_api.py now parses each
declaration on its own and is shared with Maul3D.x == x let infinities through. They now
use finite checks, so infinite positions, velocities and parameters
are refused like NaN._Static_assert, which C++ and MSVC's
default C mode reject, so a consumer that did not ask for C11 could
not include them. The layout checks moved into the library's own
sources.src/cpu.c, compiled without the backend's architecture flags./fp:contract- switch, so floating-point
contraction was not reliably off there. Every source now turns it
off with #pragma fp_contract(off) under MSVC, and the switch is
only passed where the compiler knows it.m2World_ShatterBody reports m2_errorCapacity when the world
cannot seat every piece, and refuses a malformed piece polygon up
front. Before, a full world returned 0 silently and a bad polygon
left a shapeless piece body behind.m2CreateChain checks its points first: a NaN point or a zero-
length segment is refused as m2_errorInvalid (before, the segments
were built unchecked), and a full chain or shape pool is
m2_errorCapacity instead of m2_errorInvalid. A chain that builds
no segment no longer burns a chain id, which made a replay of the
session mint different ids.