Determinism and the step

The same calls, with the same arguments, in the same order, give the same bits: on every supported platform and compiler, on x64 and arm64, with any number of worker threads, whatever floating-point mode the host's threads run in. Rollback, lockstep multiplayer, replays and server checks all rest on that.

Comparing two worlds

uint64_t hashA = m3World_Hash(worldA);
uint64_t hashB = m3World_Hash(worldB);
m3BodyDiff rows[8];
int32_t differing = 0;
m3Result result = m3World_Compare(worldA, worldB, rows, 8, &differing);

m3World_Hash folds a world's whole simulation state into 64 bits: the step count, gravity and any setting off its default, every id pool, and every body, shape, voxel grid, character, soft body, vehicle, joint, water volume and persistent contact. What follows from content alone and what only observes is left out: trees, events, counters and the profile. Equal hashes mean equal worlds. When two worlds should agree and do not, m3World_Compare lists the body slots that differ, worst first, by position, rotation and velocity.

What is promised, and what is not

Promised: one build of Maul3D, given the same sequence of calls with the same values, reaches the same state bit for bit. CI checks it on every change across twelve cells: GCC 14 on x64 and arm64 Linux, Clang 20 on x64 Linux, both on 32-bit x86 Linux, clang-cl on x64 and arm64 Windows, Clang on arm64 macOS, a shared library, Debug builds under AddressSanitizer on the scalar backend and under ThreadSanitizer, and WebAssembly. Every cell must print the hashes committed in test/hashes.txt.

Not promised:

The host's part: make the same calls in the same order with the same values. Never feed the simulation wall time, pointer values, hash-map order or thread scheduling. Step at a fixed time step.

Floating point

The build refuses fast math: a -ffast-math, /fp:fast or -Ofast in the flags stops the configure step. It compiles with floating-point contraction off, so no compiler fuses a multiply and an add on one target and not on another. The AVX2, NEON and scalar kernels do the same operations in the same order, and a test holds them to the same bits.

Every call that computes runs in the library's own floating-point setting, whatever the calling thread's is: round to nearest, subnormals kept, every exception masked. It hands the thread back its rounding mode and its flush-to-zero and denormals-are-zero bits as it found them. The host code a world calls, the allocator, the task hooks and the debug draw callbacks, runs in the thread's own setting. A task range sets the library's setting on whichever thread runs it, and the pre-solve callback runs in the library's, since its answer decides contacts.

The step

m3World_Step(world, dt, substeps) advances the world by dt seconds, from 1e-4 to 10, in 1 to 256 substeps, and returns m3_success. A step never asks the allocator, so it cannot fail for lack of memory: the world sets aside what a step needs when it is created, sized by its capacities. A step that finds more broad-phase pairs than pairCapacity still runs whole, keeping the pairs of the lowest shape indices for that step (limits).

Each step runs, in order: the vehicles' wheels; the broad and narrow phases, which update the contact pairs and their begin and end events; the solver, whose substeps each integrate velocities, warm start the joints and contacts from the impulses they kept, solve joints before contacts, integrate positions and relax; restitution; continuous collision for fast bodies; sleep; the move events; the characters riding their floors; and last the soft bodies, in substeps of their own.

A dynamic body sleeps when its speed stays below its sleep threshold, 0.05 m/s by default, for half a second, and all bodies of its island sleep together. A sleeping island does not change by a single bit until a contact, a joint change, a command or a waking neighbour wakes it. enableSleep in the world def turns sleep off.

Calls that change a world are made between steps. A call made from host code the world is running during a step, a task, a callback or an allocator, is refused with m3_errorBusy.

Threads

The library never starts a thread. A host that wants a step spread over its own threads passes two hooks in the world def, both or neither:

Each piece writes only its own slots, so any split on any number of threads gives the same bits, and CI compares one worker with four on every change. A piece can run the narrow phase, whose deepest call keeps under 48 KiB of stack, so give the threads that run pieces at least 64 KiB.

Separate worlds share nothing: two worlds can step on two threads at once and land on the same bits as stepped one after the other. Between steps, any number of threads may read one world at once through its queries, getters and snapshot; reading a world while it steps is undefined.

Derived data

What can be recomputed from content is never state: a mesh's tree, a body's mass and inertia, a voxel chunk's merged boxes. It is not in a snapshot or the hash, and it is rebuilt wherever content arrives, at create, replay and restore, by functions that give the same bits every time, so two worlds built the same way agree on it.