How to Use Box3D Recording and Replay to Capture and Reproduce Simulation State

Box3D can record a running simulation into a memory buffer and later replay it verbatim using the b3Recording API and b3RecPlayer system.

The erincatto/box3d physics engine provides deterministic recording and replay functionality that captures complete simulation state. This capability enables debugging complex physics interactions, validating determinism across different builds, and creating reproducible test cases. The system operates through a three-phase workflow: creating a recording buffer, logging world-mutating operations during simulation, and replaying the captured stream either headlessly or through an interactive player.

Understanding the Recording Architecture

Box3D implements recording as a snapshot-plus-stream architecture. When you initiate recording, the engine captures a raw image of the entire world state, then logs every subsequent API call that modifies that state.

The Recording Buffer

The b3Recording object manages a dynamically growing memory buffer that stores simulation data. You allocate this buffer through b3CreateRecording, which accepts a pre-size hint (use 0 for the default 64 KiB). The buffer automatically expands as the simulation generates more operations, or you can pre-allocate if you know the expected recording size.

What Gets Captured

The initial snapshot serializes all world data including bodies, shapes, joints, and contacts. Subsequent steps record world-mutating API calls as structured operations. According to the implementation in src/recording.c, the b3StartRecordingIntoBuffer function creates this baseline snapshot before the operation stream begins.

Complex geometry that cannot be stored as plain POD—such as convex hulls, triangle meshes, height-fields, and compound shapes—is interned into a registry inside the recording. These structures are rebuilt automatically during replay through internal types like b3RecInternHull and b3RecInternMesh.

Recording a Simulation Session

To capture a simulation, you must start recording at a step boundary before running the world. The workflow follows this sequence:

  1. Create a world and populate it with bodies
  2. Allocate a b3Recording buffer
  3. Call b3World_StartRecording to snapshot the world and begin logging
  4. Run the simulation normally
  5. Stop recording and persist the data
// Create a world
b3WorldId world = b3CreateWorld(&worldDef);

// Allocate a recording buffer (0 = default 64 KiB)
b3Recording* rec = b3CreateRecording(0);

// Start recording – must be at a step boundary
b3World_StartRecording(world, rec);

// Run the simulation as usual
b3World_Step(world, dt, subSteps);
// Create bodies, apply forces, etc.

// Stop recording (optional – destroying the world also detaches it)
b3World_StopRecording(world);

// Persist the data to disk
b3SaveRecordingToFile(rec, "session.b3rec");
b3DestroyRecording(rec);

The recording captures every world-modifying operation from the start point until you call b3World_StopRecording or destroy the world. Access the raw bytes directly via b3Recording_GetData and b3Recording_GetSize if you need custom serialization rather than file-based storage.

Replaying Recorded Simulations

Box3D provides two distinct replay paths depending on your use case: headless validation for automated testing and interactive playback for debugging.

Headless Validation with b3ValidateReplay

For automated tests and determinism checking, use the b3ValidateReplay function. This re-runs the engine on a single worker and returns true only if every recorded operation and per-step state hash matches exactly. This path is implemented in src/recording_replay.c.

const uint8_t* data = b3Recording_GetData(rec);
int size = b3Recording_GetSize(rec);
bool ok = b3ValidateReplay(data, size, 1);   // true → exact replay

The function takes the raw recording bytes, the size, and a worker count. If the simulation reproduces identically, the function returns true; otherwise, it identifies where the replay diverged.

Interactive Playback with b3RecPlayer

For visual debugging or frame-by-frame analysis, create a b3RecPlayer using b3RecPlayer_Create. The player owns its own copy of the recording bytes, builds a replay world, and exposes navigation controls.

// Load a recording from disk
b3Recording* loaded = b3LoadRecordingFromFile("session.b3rec");

// Create a player; workerCount = 1 for a serial replay
b3RecPlayer* player = b3RecPlayer_Create(
    b3Recording_GetData(loaded),
    b3Recording_GetSize(loaded),
    1);

b3WorldId replayWorld = b3RecPlayer_GetWorldId(player);

// Optional: install debug-shape callbacks for visualization
b3RecPlayer_SetDebugShapeCallbacks(player,
                                     myCreateDebugShape,
                                     myDestroyDebugShape,
                                     myContext);

// Step through the recording frame-by-frame
while (b3RecPlayer_StepFrame(player))
{
    // The replay world now holds the state after this frame
    // Use normal query APIs (b3Body_GetPosition, b3World_Draw, …)
}

// Navigation controls
b3RecPlayer_SeekFrame(player, 42);   // jump to frame 42
b3RecPlayer_Restart(player);        // rewind to frame 0

// Test multithreaded determinism by changing worker count
b3RecPlayer_SetWorkerCount(player, 4);

// Clean up
b3RecPlayer_Destroy(player);
b3DestroyRecording(loaded);

The player API defined in include/box3d/box3d.h and src/recording_replay.h supports seeking to arbitrary frames, restarting from the beginning, and modifying the worker count to test parallel determinism.

Key Implementation Details and Limitations

The recording system enforces strict determinism requirements. The replay build must have identical struct layouts, pointer width, endianness, and floating-point environment as the build that created the recording. As implemented in b3RecPlayer_Create, a layout hash check rejects recordings that violate these constraints.

User data (raw pointers stored in bodies or shapes) is not persisted—it is written as zero. If your simulation relies on user data pointers, you must re-populate them after the replay world initializes. Similarly, host callbacks such as custom friction mixers, pre-solve functions, and filter callbacks are omitted from the recording. The default mixers are pure functions, so deterministic replay works as long as you do not replace them with custom implementations.

Summary

  • Box3D recording captures simulation state through a snapshot-plus-op-stream architecture using the b3Recording buffer API in src/recording.h.
  • Start recording by calling b3World_StartRecording at a step boundary, then persist with b3SaveRecordingToFile or access raw bytes via b3Recording_GetData.
  • Validate determinism headlessly using b3ValidateReplay, which checks that every operation and state hash matches the original recording.
  • Debug interactively using b3RecPlayer_Create, which provides frame stepping, seeking, and worker-count adjustment for testing parallel execution.
  • Maintain binary compatibility between recording and replay builds—struct layouts, pointer widths, and floating-point environments must match exactly.

Frequently Asked Questions

What is the file format for Box3D recordings?

Box3D recordings are memory buffers containing a custom binary format that starts with a layout hash and snapshot followed by an operation stream. While you can write these to disk with any extension, the convention is .b3rec. The format is not human-readable and requires the b3RecPlayer or b3ValidateReplay functions to interpret.

Can I replay a recording on a different architecture?

No. The determinism contract requires identical struct layouts, pointer width, endianness, and floating-point environment between the recording and replay builds. The loader in b3RecPlayer_Create explicitly checks a layout hash and rejects incompatible recordings to prevent undefined behavior.

How does Box3D handle complex geometry in recordings?

Complex geometry such as convex hulls, triangle meshes, and height-fields cannot be stored as plain POD data. The system interns these structures into a registry within the recording buffer using types like b3RecInternHull and b3RecInternMesh, then automatically rebuilds them during replay initialization.

Why is my user data missing after replay?

User data pointers are intentionally zeroed out during recording because they represent host-specific memory addresses that would be invalid in a replay context. You must manually re-associate user data with bodies and shapes after creating the replay world, typically by iterating through the world and restoring pointers based on body IDs or other persistent identifiers.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →