A snapshot is the whole simulation in a byte buffer. Restoring it puts a world back exactly where it was, and the world then steps to the same bits it would have reached anyway. Rollback netcode, instant replays and save states are built on that.
int64_t size = m3World_GetSnapshotSize(world);
uint8_t* buffer = malloc((size_t)size);
int64_t written = 0;
m3Result result = m3World_Snapshot(world, buffer, size, &written);
The world steps on; when a correction arrives, it goes back:
if (result == m3_success)
{
result = m3World_Restore(world, buffer, written);
}
free(buffer);
m3World_GetSnapshotSize gives the exact size of a world's snapshot
now. It grows with the world's content, meshes and hulls above all, so
measure it before each snapshot rather than once per session. A buffer
smaller than that is refused with m3_errorCapacity; any failed
snapshot leaves written at 0 and zeroes the buffer, so no part of it
reads as a snapshot.
Restore works in place, into a world with the same capacities as the one that wrote the snapshot. Worlds of different capacities are different shapes of state, and restore refuses them.
Everything that decides what the next step does: every body, shape, joint, character, vehicle, soft body, water volume and voxel chunk; the hulls and meshes their shapes use; the id pools with their free lists and generations, so an id taken before a snapshot names the same object after a restore; the contact pairs with their stored impulses; the broad-phase tree; sleep state; the world's settings; the wind's phase; the step counter.
Three things are left out on purpose:
A snapshot is read by the releases of the minor version that wrote it,
built with the same settings. Its header carries the writer's
M3_ABI_VERSION, the format number, and a hash of the solver revision,
the float sizes and the floating-point policy. A snapshot from another
minor version, from a build of other settings, or from before a release
that changed what a step computes is refused with m3_errorVersion. A world of other capacities, a
truncated buffer or a damaged one is refused with m3_errorFormat.
Restore treats the bytes as untrusted. Before it changes anything it
checks every block: indices, counts, flags and finite values; then the
structures as wholes, the id pools, the lists that tie bodies to
shapes and joints, the broad-phase tree, the contact pairs, the shared
hulls and meshes with their reference counts, and every character,
vehicle, soft body and joint record. Any value past the limit its
create or setter keeps is refused too (the magnitude
limits). A snapshot the engine could not
have written is refused with m3_errorFormat, and the world is left
exactly as it was. A refusal of the bytes counts no misuse; a world
that accepts a snapshot steps, queries and snapshots like one built
through the API.
A snapshot is a save state for rollback and replay, not an archive. There is no migration between versions: keep the game's own state, not snapshots, across an upgrade.
Snapshot sizes are 64-bit and have no limit of their own: a world
that fits in memory can be snapshotted. What is promised is that
m3World_GetSnapshotSize gives a world's exact size; ask it rather
than computing one.
As a guide for budgets, these are the sizes of release 1.0.0. They
were measured on x86-64 Linux with a clang-20 build, by comparing
m3World_GetSnapshotSize across capacities; the format has one
layout on every target, so every target gives the same sizes. Most of
a snapshot is set by the capacities, used or not: 1924 bytes for each
shape slot, 12,821 for each voxel slot and 99,659 for each soft body
slot. Content adds to that, by the format's layout in 1.0.0: about
1.31 MB for a mesh of 65,535 vertices and 65,535 triangles, 5.8 KB for
a hull of 64 vertices and 124 faces, and 260 KB for a native height
field of 255 by 255 samples. A later release may change these sizes,
and the format's number changes with them.
On a 32-bit target, m3World_Restore, m3World_StartJournal,
m3World_ReplayJournal and the replay container calls refuse a size
past SIZE_MAX with m3_errorInvalid, before they form a pointer.
A snapshot, like a journal tape, is little-endian, with every value laid out as on x86-64, eight-byte values eight-byte aligned. The build pins the size and member offsets of every type a snapshot carries, so a compiler that would lay one out differently fails the build rather than write another format. A snapshot written on any supported target restores on any other. The state formats describe the bytes.
The bit-exact promise makes the classic rollback recipe work exactly as written:
m3World_Hash between peers at agreed frames. Equal hashes
prove the peers agree; a difference shows the frame where they
parted, and m3replay diff names the bodies that carried it.tools/m3lockstep runs this recipe as a program: an online peer that
predicts and rolls back, against an offline peer that steps only on
inputs it has, over a link that delays each input by one to four
frames at 60 Hz, for 300 frames. Every 60 frames it checks that both
peers hold the same hash, and it fails when they do not; CI runs it
on every change. In release 1.0.0 a run rolls back 303 times, at most
four frames deep. Input delay, the rollback
window and how to guess inputs are the host's choices; the engine's
part is that the same inputs give the same bits.