Repository navigation
fix(snapshot): restore refuses a wheel direction that is not unit length - #415
Merged
Merged
Conversation
siracozmen01
commented
Nov 5, 2026
siracozmen01
left a comment
Collaborator
Author
There was a problem hiding this comment.
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 storesdirection * (1 / sqrtf(dot)), and a slot past the wheel count gets exactly (0, -1, 0).m3DestroyVehicleInternalzeroes the direction (line 223), but only for a vehicle that is no longer alive.VehiclesValidskips any slot that is not alive, andWheelsValidwalks onlyw < 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
m3Dot3rounding 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, whichm3WheelDefCheck'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.
Merged
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))
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
m3CreateVehicletakes a wheel direction whose squared length lies between 0.81 and 1.21 and stores it normalized. That happens inInitWheelinsrc/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_Restorechecked the stored direction only with the create's own check:WheelsValidinsrc/snapshot_bounds.ccallsm3WheelDefCheck. It therefore took any direction in that wide band. On main, a snapshot whose stored wheel direction was scaled to length 0.95 restored withm3_success, so a restored wheel could push along a vector the library never stores (#414).Change
WheelsValidalso requires the stored direction's squared length to lie within 1e-5 of 1, so restore refuses withm3_errorFormatany 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
TestWheelDirectionLengthintest/test_restore_bounds.cedits the first wheel's stored direction in a snapshot:WheelsValid, both refusals fail in that test. With it, gcc-14 Debug with-DMAUL3D_WERROR=ONpasses all 77 tests.tools/check.sh prpasses in the main checkout on this head: 77 and 75 tests, and fuzz with no finding.Closes #414