Skip to content

fix(snapshot): restore refuses a wheel direction that is not unit length - #415

Merged
siracozmen01 merged 1 commit into
mainfrom
fix/wheel-direction-restore
Nov 5, 2026
Merged

siracozmen01 merged 1 commit into
mainfrom
fix/wheel-direction-restore

Conversation

@siracozmen01

Copy link
Copy Markdown
Collaborator

Problem

m3CreateVehicle takes a wheel direction whose squared length lies between 0.81 and 1.21 and stores it normalized. That happens in InitWheel in src/vehicle.c, the only call that writes a live wheel's direction. The step then uses the stored direction as a unit vector for the suspension's cast and forces.

m3World_Restore checked the stored direction only with the create's own check: WheelsValid in src/snapshot_bounds.c calls m3WheelDefCheck. It therefore took any direction in that wide band. On main, a snapshot whose stored wheel direction was scaled to length 0.95 restored with m3_success, so a restored wheel could push along a vector the library never stores (#414).

Change

WheelsValid also requires the stored direction's squared length to lie within 1e-5 of 1, so restore refuses with m3_errorFormat any direction create would not have stored.

The band is set by measurement. Over 20 million random directions in the create band, the worst normalized one is 4.17e-7 from unit squared length. That leaves the band about 24 times wider than what the normalization leaves, so no state the library writes is refused.

No step, result or golden hash moves: the check only refuses snapshots the library cannot produce.

Verification

  • TestWheelDirectionLength in test/test_restore_bounds.c edits the first wheel's stored direction in a snapshot:
    • -1 and -1.000002 along its axis restore;
    • -0.95, which create takes and would normalize, and -1.00001, just past the band, are refused with the world left as it was and no misuse counted.
  • With the unit check removed from WheelsValid, both refusals fail in that test. With it, gcc-14 Debug with -DMAUL3D_WERROR=ON passes all 77 tests.
  • tools/check.sh pr passes in the main checkout on this head: 77 and 75 tests, and fuzz with no finding.

Closes #414

@siracozmen01 siracozmen01 left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Review at 8da28de: ready, with one point not verified. No findings. Read, not run.

Writers of a wheel direction, as far as I can read them:

  • InitWheel (vehicle.c:138) is the only writer in vehicle.c for a live wheel. Its single caller is create (line 195). For a def wheel it stores direction * (1 / sqrtf(dot)), and a slot past the wheel count gets exactly (0, -1, 0).
  • m3DestroyVehicleInternal zeroes the direction (line 223), but only for a vehicle that is no longer alive.
  • VehiclesValid skips any slot that is not alive, and WheelsValid walks only w < wheelCount. So the zero and the past-count slots never meet the new band.
  • The public vehicle API (vehicle.h) has no setter for a wheel's geometry: only commands, tank commands, drivetrain and gear.
  • Restore copies the stored bytes, which is exactly what this check now bounds.

The band:

  • A float normalization leaves the squared length off by a few ulps of 1, around 1e-6 at worst. That includes m3Dot3 rounding at the check, and a snapshot taken on a platform that contracts to FMA differently. The 4.17e-7 measured is consistent with that, and 1e-5 is wide of it.
  • !(d2 > ... && d2 < ...) also refuses NaN, which m3WheelDefCheck's finiteness test already did.

Test:

  • -0.95 (d2 = 0.9025) and -1.00001 (d2 about 1.00002) both lie in create's 0.81 to 1.21 band, so main restores them. Both refusal assertions therefore fail on main for the right reason.
  • -1.000002 (d2 about 1.000004) checks that the band does not refuse a near-unit direction.
  • The world-unchanged and no-misuse checks match restore's contract.

Not verified: that no source outside vehicle.c and the snapshot code writes vehWheelDir (I read vehicle.c whole, but cannot search the tree). A search for vehWheelDir across src/ that finds only vehicle.c's two writes and the snapshot's read and copy would settle it.

The body has Problem, Change and Verification.

@siracozmen01
siracozmen01 merged commit 110ceec into main Nov 5, 2026
28 checks passed
@siracozmen01
siracozmen01 deleted the fix/wheel-direction-restore branch November 5, 2026 11:19
@rawframe-release rawframe-release Bot mentioned this pull request Nov 5, 2026
siracozmen01 pushed a commit that referenced this pull request Nov 8, 2026
🤖 I have created a release *beep* *boop*
---


## [1.1.0](v1.0.5...v1.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](#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](#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](#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](#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](#416))
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.

restore takes a wheel direction that is not unit length, which create never stores

1 participant