Skip to content

build(deps)!: move Maul2D to 1.3.1 - #225

Merged
siracozmen01 merged 2 commits into
mainfrom
deps-maul2d-1.3.1
Nov 5, 2026
Merged

siracozmen01 merged 2 commits into
mainfrom
deps-maul2d-1.3.1

Conversation

@siracozmen01

@siracozmen01 siracozmen01 commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

Problem

The engine pins Maul2D 0.6.0. Maul2D 1.3.1, the latest release (tag v1.3.1, cf08f1f), is released and verified. Between them lie nine 0.x releases (0.7.0 to 0.11.4), five of them breaking, then 1.0.0, which settles the API, and the 1.1, 1.2 and 1.3 minor releases with their patches.

Change

The engine moves to Maul2D 1.3.1, vendored from its release archive checked against the release's SHA256SUMS with tools/update_maul.sh.

  • third_party/maul2d/: upstream's include/, src/ and LICENSE at 1.3.1. The engine's own CMakeLists.txt gains src/snapshot_pairs.c, the one source file upstream added; its flags already match upstream's (-ffp-contract=off, no fast math, C23, AVX2 and FMA kernels on x64).
  • modules/physics2d/: the calls the renames and reorders below touch, and two status checks the library now returns.
  • docs/operations/dependencies.md: the pin row.
  • docs/internals/physics-and-navigation.md: the tick lengths the 2D step takes, and the kinematic target it does not carry out.
  • docs/user/dedicated-server.md: the world.tick_rate a game with 2D physics takes.
  • Tests: a tick of a minute and one of a two-millionth of a second are refused by the step system before anything changes, and a ten-second tick steps; a kinematic target 3 km away in one tick is cleared and not counted.

What moves

  • modules/physics2d/tests/physics_test.cpp: the 2D physics digest golden moves from 0xc42bbb4bf908bd7f to 0x57dd106d48ba3973. The scene's 20 ball poses after 120 ticks are the same bits on 0.6.0 and 1.3.1 (an FNV-1a hash of all of them is 17cf21929a83818f on both), so only what Maul2D's world hash covers moved: the broadphase boxes (0.11.4) and the sleep thresholds (1.3.0).
  • Physics2DStatistics::targets counts only targets carried out (0.9.0 below).
  • The 2D step fails at a tick it cannot step (0.9.0 below).

Each release and what it does to the engine

Every engine call into Maul2D is in modules/physics2d/src/ (physics.cpp, characters.cpp, joints.cpp). The engine never writes a Maul2D snapshot or journal tape and keeps none on disk, so no format change below reaches a file of ours.

0.7.0

  • Breaking: the list queries take their filter before the array. Engine call sites: m2World_OverlapCircle twice in Physics2D::overlapCircle and m2World_CollideMover in characters.cpp, all reordered. Nothing they return changes.
  • Breaking: renames. M2_MOVER_PLANES is M2_MAX_MOVER_PLANES (16) in characters.cpp; maxMotorTorque (revolute) and maxMotorForce (prismatic) are maxMotorEffort in joints.cpp. Same meaning, same values.
  • No result moves.

0.8.0

  • Breaking: renames. m2CpuSupportsBackend is m2IsCpuBackendSupported (it returns bool from 0.9.0), and m2BodyDef.isBullet is enableBullet, both in physics.cpp. The engine calls none of the other renamed functions.
  • No result moves.

0.9.0

  • Breaking: m2World_Step returns m2Result and must be read. The engine checks the tick's length against Maul2D's M2_MIN_STEP to M2_STEP_LIMIT (1 microsecond to 10 seconds) at the start of the step system, before it changes anything, and fails the system with Physics2DError::InvalidSettings outside it. On 0.6.0 such a tick was refused inside Maul2D without a word and the world stood still. Substeps were already held to 1 to 64, which is M2_MAX_SUBSTEPS, so a refused step after that check is an engine fault and stops the process (RAWFRAME_CHECK). Every configured rate the engine ships (60 Hz and the tests' rates) is inside the range.
  • Breaking: m2Body_SetTargetTransform returns m2Result. A target too far to reach within M2_SPEED_LIMIT (100 km/s) in one tick was refused on 0.6.0 too, without a status, and Physics2DStatistics::targets counted it anyway. The engine now counts only targets carried out, as that field's comment says. The body still stays where it was, and the target is still cleared.
  • Breaking: the counters, m2BodyType as an int32_t typedef, the water volume rename, the removed math helpers. The engine reads no counter or profile, uses no water volume or particle, and casts to m2BodyType, which still compiles.
  • Breaking: a refused list read zeroes the caller's array, and the closest casts zero hitOut on a miss. The engine reads a hit only when the call returns true, and a list only up to the count returned, so neither reaches it.
  • No result moves.

0.10.0

  • Breaking: a list read refuses a null array with a capacity above 0. Every engine list read passes a vector's storage, resized to at least 16 or 64 first, with its size.
  • Breaking: particle settings are checked when there are no particles. The engine's world def keeps Maul2D's defaults for them.
  • m2ComputeHull keeps fine curves. The engine's polygons are boxes and tile rectangles of four corners each, whose hulls are the same.
  • No result moves.

0.11.0

  • Breaking: snapshot format 8 and journal format 9. The engine writes neither.
  • No simulation result moves.

0.11.1

  • Two shapes without area never collide. The engine makes no segment or chain shape, so no engine body changes.

0.11.2 and 0.11.3

  • A particle near many shapes, a bullet among more than 64 shapes, and m2World_CollideMover with a short array. The engine has no particles. A bullet body (Body2D::bullet) with more than 64 shapes along one substep's sweep now stops at a wall it could pass before. A character whose capsule meets more than 16 planes keeps those of the lowest shape slots instead of the first the trees yield; with 16 or fewer it gets the same planes in the same order.

0.11.4

  • Result moves: the step refits moving shapes' broadphase boxes at its end, so a query between steps sees each shape where the last step left it. Body state is bit-identical, but m2World_Hash covers the boxes and moved flags, so Physics2D::digest moves for any world with a moving body. The engine's queries run between steps: Kest's rays, sweeps and overlaps, castRayAt, and each character's moves, which collide its capsule against the world before the step. A shape that moved more than 0.1 m in the last step is now found where it is, instead of missed or found where it was.

1.0.0 and 1.0.1

  • No engine change. 1.0.1 fixes m2DecomposeOutline, which the engine does not call.

1.1.0

  • Results: m2DecomposeOutline returns new pieces. The engine does not call it.

1.2.0

  • Results: water volume buoyancy far from the origin. The engine makes no water volume.

1.2.1

  • A static shape moved into a sleeping body touches it. The engine moves a static body only by making it again at its new pose (a teleport), which already made a fresh pair, so no engine result moves.

1.2.2

  • A body whose rotation is locked holds still. A Body2D with fixedRotation given an angular velocity (by Velocity2D or an angular impulse) no longer turns. On 0.6.0 it turned at that speed with nothing to slow it.

1.3.0

  • Results: the world hash covers each body's sleep threshold, so Physics2D::digest moves for every world. The engine keeps the default threshold, so no body sleeps differently.
  • New calls, none used yet.
  • M2_NODISCARD works under GCC 14's C23. It reaches the two calls above, which the engine now reads.

1.3.1

  • Internal bool results checked inside Maul2D. Nothing the engine sees changes.

Verification

  • physics2d's tests: with the old golden, TheSameWorldAndWritesGiveTheSameBits failed only at the digest. A probe hashed the scene's 20 ball poses after 120 ticks on both builds: 17cf21929a83818f on 0.6.0 and on 1.3.1. The new digest holds in every tree below, so the bytes are the same on Clang, GCC 14 and wasm32.
  • tools/check.sh full passed at four jobs through the heavy-job wrapper, from cold trees, in 3850 s:
    • clang-thread: 208 of 208.
    • clang-sanitize: 253 of 253.
    • gcc-debug (GCC 14): 208 of 208.
    • clang-development: 208 of 208 unit tests and 19 of 19 integration tests, the played games among them.
    • wasm-development: 62 of 62.
  • The organization's documentation check passed on the branch.
  • Maul2D's 68 sources and its CPU check, cleaned and built again in the gcc-debug tree with GCC 14 and Maul2D's own -Wall -Wextra: 0 warnings, so 1.3.0's working M2_NODISCARD finds no dropped status in Maul2D.

BREAKING CHANGE: the engine is built on Maul2D 1.3.1. The 2D physics digest changes for every world, since Maul2D's world hash now covers each shape's broadphase box and each body's sleep threshold. Queries between steps, character moves among them, see a shape that moved more than 0.1 m in the last step where it is now. A body with a locked rotation no longer turns when given a spin. The 2D step fails at a tick shorter than 1 microsecond or longer than 10 seconds instead of standing still, and the physics statistics count only kinematic targets carried out.

Vendor Maul2D 1.3.1 from its release archive and follow its renames,
reordered list queries and new statuses. The 2D step refuses a tick
Maul2D cannot step before it changes anything, and the statistics
count only kinematic targets carried out.

BREAKING CHANGE: the engine is built on Maul2D 1.3.1. The 2D physics
digest changes for every world, since Maul2D's world hash now covers
each shape's broadphase box and each body's sleep threshold; body poses
are unchanged. Queries between steps see a shape that moved more than
0.1 m in the last step where it is now. A body with a locked rotation
no longer turns when given a spin. The 2D step fails at a tick shorter
than 1 microsecond or longer than 10 seconds, and the statistics count
only kinematic targets carried out.

@siracozmen01 siracozmen01 left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review at 2342a0b: no change needed. I read the engine's changes and the Maul2D parts they depend on; I did not run anything.

  • The step check is exactly Maul2D's own. M2_MIN_STEP is 1.0e-6f and M2_STEP_LIMIT is 10.0f (third_party/maul2d/include/maul2d/base.h:266-267). Maul2D's m2CheckTimeStep (src/core.h:267, through m2CheckPositive and m2CheckRange) accepts x > 0 && x >= lo && x <= hi. The engine compares the same float kSeconds against the same float constants with >= and <=, so the two agree at both ends and on NaN.
    • The other refusals in WorldStepWorker are: substeps (already held to 1 to 64, and M2_MAX_SUBSTEPS is 64, base.h:206), and m2CheckWriter (src/world_internal.c:131), which refuses only a null world or a call from inside Maul2D's host code. The step system is neither, so the RAWFRAME_CHECK cannot fire on a checked tick.
    • Moving kSeconds above the body sweep makes the refusal come before anything changes, and the test's bodiesMade == 0 pins that.
  • Kinematic targets. m2Body_SetTargetTransform now returns a status, and the engine counts only m2_success. The new test case moves the paddle by about 3 km in one tick, which is over 100 km/s at the scene's rate. It is refused and cleared, and the unchanged targets == 31 would be 32 on the old counting, so the assertion can fail. The paddle staying at x = 3 pins that the body did not move.
  • The calls 0.7.0 to 0.9.0 reorder and rename are all updated: m2World_OverlapCircle (both calls), m2World_CollideMover, M2_MAX_MOVER_PLANES, maxMotorEffort on the revolute and prismatic defs, enableBullet, and m2IsCpuBackendSupported read as bool. The 0.10.0 null-array refusal does not reach overlapCircle, since its vector is resized to at least 16 first.
  • CMakeLists.txt. In the pull request's file list, src/snapshot_pairs.c is the only new .c under third_party/maul2d/src/. The two other new files, snapshot_pairs.h and snapshot_read.h, are headers, and no source is removed, so the one added line is the whole change to the source list. I did not compare against the archive itself; that rests on your byte comparison.
  • Body. It has no column-0 word( or type(scope): line, and its BREAKING CHANGE: footer covers the four behavior changes a game can see.

@siracozmen01
siracozmen01 merged commit f406e36 into main Nov 5, 2026
11 of 12 checks passed
@siracozmen01
siracozmen01 deleted the deps-maul2d-1.3.1 branch November 5, 2026 14:14
@rawframe-release rawframe-release Bot mentioned this pull request Nov 5, 2026
siracozmen01 pushed a commit that referenced this pull request Nov 5, 2026
🤖 I have created a release *beep* *boop*
---


## [0.8.0](v0.7.0...v0.8.0) (2026-11-05)

This release moves the engine to Maul2D 1.3.1. Body poses step to the same bits as before, but the 2D physics digest changes for every world, and a few edge cases of 2D physics act differently. No wire, save, checkpoint or cooked format changes.

### Changed

- **Breaking:** The engine is built on Maul2D 1.3.1, from 0.6.0, vendored from its release archive with its SHA-256 checked. What a game sees change ([#225](#225)):
  - The 2D physics digest changes for every world, since Maul2D's world hash now covers each shape's broadphase box and each body's sleep threshold. Body poses are unchanged: a scene of 20 bouncing balls ends on the same bits after 120 ticks.
  - Rays, sweeps, overlaps and character moves between steps see a shape that moved more than 0.1 m in the last step where it is now, instead of missing it or finding it where it was.
  - A body with a fixed rotation no longer turns when a game gives it an angular velocity or an angular impulse.
  - A bullet body passes no wall when more than 64 shapes crowd its path, and a character meeting more than 16 surfaces keeps those of the lowest shape slots.
  - The 2D step fails at a tick shorter than 1 microsecond or longer than 10 seconds, before it changes anything, instead of leaving the world standing still. A dedicated server with 2D physics takes a `world.tick_rate` of 1 to 1,000,000.
  - `Physics2DStatistics::targets` counts only kinematic targets carried out. A target too far to reach in one tick within Maul2D's 100 km/s speed limit is cleared, as before, but no longer counted.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant