Box2D-0.1.0.0: upstream/src/snapshot.c
// SPDX-FileCopyrightText: 2026 Erin Catto
// SPDX-License-Identifier: MIT
#include "snapshot.h"
#include "bitset.h"
#include "body.h"
#include "broad_phase.h"
#include "constraint_graph.h"
#include "contact.h"
#include "container.h"
#include "core.h"
#include "dynamic_tree.h"
#include "id_pool.h"
#include "island.h"
#include "joint.h"
#include "physics_world.h"
#include "recording.h"
#include "sensor.h"
#include "shape.h"
#include "solver_set.h"
#include "table.h"
#include "box2d/box2d.h"
#include "box2d/collision.h"
#include "box2d/types.h"
#include <string.h>
// Snapshot image magic and version
#define B2_SNAP_MAGIC 0x32534E42u // 'BNS2'
// Bump this if any of the data structures below get modified. The layout hash only catches
// size changes, a same-size reinterpretation like the contact cache reshape needs this bump.
#define B2_SNAP_VERSION 13u // extend b2Capacity
// Header flag bits
#define B2_SNAP_FLAG_VALIDATION 0x1u // image was built with validation, only used for diagnostics
#define B2_SNAP_FLAG_DOUBLE_PRECISION 0x2u // image was built with double precision world positions
// Layout hash seeds from all structs we memcpy, plus key constants.
// Changing any struct or constant updates the hash. Re-purposing or swapping
// fields might not change the hash
static uint32_t b2ComputeLayoutHash( void )
{
// FNV-1a hash
uint32_t h = 2166136261u;
#define MIX( x ) \
h ^= (uint32_t)( x ); \
h *= 16777619u;
MIX( sizeof( b2Body ) )
MIX( sizeof( b2BodySim ) )
MIX( sizeof( b2BodyState ) )
MIX( sizeof( b2Shape ) )
MIX( sizeof( b2ChainShape ) )
MIX( sizeof( b2Contact ) )
MIX( sizeof( b2ContactSim ) )
MIX( sizeof( b2Joint ) )
MIX( sizeof( b2JointSim ) )
MIX( sizeof( b2Island ) )
MIX( sizeof( b2IslandSim ) )
MIX( sizeof( b2ContactLink ) )
MIX( sizeof( b2JointLink ) )
MIX( sizeof( b2Sensor ) )
MIX( sizeof( b2Visitor ) )
MIX( sizeof( b2SolverSet ) )
MIX( sizeof( b2GraphColor ) )
MIX( sizeof( b2DynamicTree ) )
MIX( sizeof( b2TreeNode ) )
MIX( sizeof( b2TreeProxy ) )
MIX( sizeof( b2SetItem ) )
MIX( sizeof( b2IdPool ) )
MIX( sizeof( b2SurfaceMaterial ) )
MIX( B2_GRAPH_COLOR_COUNT )
MIX( b2_bodyTypeCount )
MIX( sizeof( void* ) )
#undef MIX
return h;
}
// Snapshot image header, written at offset 0
typedef struct b2SnapHeader
{
uint32_t magic;
uint32_t version;
uint32_t layoutHash;
uint32_t flags; // B2_SNAP_FLAG_*, keeps 16-byte alignment
} b2SnapHeader;
// Compile-time tripwires for the structs serialized field-by-field. If one fires, a field was added
// or reordered: resync the matching code in b2SerializeWorld / b2DeserializeIntoShell and bump
// B2_SNAP_VERSION.
#if INTPTR_MAX == INT64_MAX
_Static_assert( sizeof( b2ChainShape ) == 32,
"b2ChainShape layout changed; resync snapshot chain serialization" );
_Static_assert( sizeof( b2Sensor ) == 56,
"b2Sensor layout changed; resync snapshot sensor serialization" );
_Static_assert( sizeof( b2Island ) == 64,
"b2Island layout changed; resync snapshot island serialization" );
#endif
// Bounds-checked read cursor, mirrors b2RecReader discipline
typedef struct b2SnapReader
{
const uint8_t* data;
int cursor;
int size;
bool ok;
} b2SnapReader;
static void b2SnapRCheck( b2SnapReader* r, int need )
{
if ( need < 0 || (int64_t)r->cursor + (int64_t)need > (int64_t)r->size )
{
r->ok = false;
}
}
static void b2SnapR_Bytes( b2SnapReader* r, void* dst, int n )
{
b2SnapRCheck( r, n );
if ( !r->ok )
{
return;
}
memcpy( dst, r->data + r->cursor, n );
r->cursor += n;
}
static int b2SnapR_I32( b2SnapReader* r )
{
int32_t v = 0;
b2SnapR_Bytes( r, &v, 4 );
return (int)v;
}
static uint32_t b2SnapR_U32( b2SnapReader* r )
{
uint32_t v = 0;
b2SnapR_Bytes( r, &v, 4 );
return v;
}
static void b2SnapW_I32( b2RecBuffer* buf, int v )
{
int32_t w = (int32_t)v;
b2RecBufAppend( buf, &w, 4 );
}
static void b2SnapW_U32( b2RecBuffer* buf, uint32_t v )
{
b2RecBufAppend( buf, &v, 4 );
}
static void b2SnapW_Bytes( b2RecBuffer* buf, const void* src, int n )
{
b2RecBufAppend( buf, src, n );
}
// Reject a count read from the image before it reaches an allocation or memset. count must be non
// negative, its in-memory footprint must fit in int, and the stream must hold at least
// minStreamBytes per element. A corrupt or truncated image then fails.
static bool b2SnapCheckCount( const b2SnapReader* r, int count, int memSize, int minStreamBytes )
{
if ( count < 0 || memSize < 0 || minStreamBytes < 0 )
{
return false;
}
if ( memSize > 0 && count > INT32_MAX / memSize )
{
return false;
}
int64_t remaining = (int64_t)r->size - (int64_t)r->cursor;
return (int64_t)count * (int64_t)minStreamBytes <= remaining;
}
// Serialize a POD array: count then raw bytes
#define b2SerPodArray( buf, arr ) \
do \
{ \
b2SnapW_I32( buf, ( arr ).count ); \
if ( ( arr ).count > 0 ) \
{ \
b2SnapW_Bytes( buf, ( arr ).data, ( arr ).count * (int)sizeof( *( arr ).data ) ); \
} \
} \
while ( 0 )
// Serialize a sparse struct array whose element carries a host userData pointer. userData is host
// wiring, not simulation state, so write it as NULL: images stay reproducible and never persist
// host addresses across a save. On restore the field reads back NULL.
#define b2SerSimArray( buf, arr, type ) \
do \
{ \
b2SnapW_I32( buf, ( arr ).count ); \
for ( int slot = 0; slot < ( arr ).count; ++slot ) \
{ \
type elem = ( arr ).data[slot]; \
elem.userData = NULL; \
b2SnapW_Bytes( buf, &elem, (int)sizeof( type ) ); \
} \
} \
while ( 0 )
// Deserialize a POD array: validate count, resize, then memcpy
#define b2DesPodArray( r, arr ) \
do \
{ \
int cnt = b2SnapR_I32( r ); \
int elemSize = (int)sizeof( *( arr ).data ); \
if ( ( r )->ok && b2SnapCheckCount( r, cnt, elemSize, elemSize ) == false ) \
{ \
( r )->ok = false; \
} \
if ( ( r )->ok && cnt > 0 ) \
{ \
b2Array_Resize( arr, cnt ); \
b2SnapR_Bytes( r, ( arr ).data, cnt * elemSize ); \
} \
else if ( ( r )->ok ) \
{ \
( arr ).count = 0; \
} \
} \
while ( 0 )
// Id pool: nextIndex + freeArray
static void b2SerIdPool( b2RecBuffer* buf, const b2IdPool* pool )
{
b2SnapW_I32( buf, pool->nextIndex );
b2SerPodArray( buf, pool->freeArray );
}
static void b2DesIdPool( b2SnapReader* r, b2IdPool* pool )
{
pool->nextIndex = b2SnapR_I32( r );
b2DesPodArray( r, pool->freeArray );
}
// BitSet: blockCount + raw uint64_t words
static void b2SerBitSet( b2RecBuffer* buf, const b2BitSet* bs )
{
b2SnapW_U32( buf, bs->blockCount );
if ( bs->blockCount > 0 )
{
b2SnapW_Bytes( buf, bs->bits, (int)( bs->blockCount * sizeof( uint64_t ) ) );
}
}
// Restore a bitset, leak-clean: destroy the existing one, alloc fresh at exact blockCount capacity.
// Always keep bits non-NULL, matching b2CreateBitSet, so a later b2GrowBitSet on a count-0 bitset
// has a buffer to grow from. An empty color bitset serializes as 0 blocks but must restore usable.
static void b2DesBitSet( b2SnapReader* r, b2BitSet* bs )
{
uint32_t blockCount = b2SnapR_U32( r );
if ( r->ok && b2SnapCheckCount( r, (int)blockCount, (int)sizeof( uint64_t ),
(int)sizeof( uint64_t ) ) == false )
{
r->ok = false;
}
b2DestroyBitSet( bs );
if ( !r->ok )
{
return;
}
uint32_t blockCapacity = blockCount > 0 ? blockCount : 1;
bs->bits = b2Alloc( blockCapacity * sizeof( uint64_t ) );
memset( bs->bits, 0, blockCapacity * sizeof( uint64_t ) );
bs->blockCapacity = blockCapacity;
bs->blockCount = blockCount;
if ( blockCount > 0 )
{
b2SnapR_Bytes( r, bs->bits, (int)( blockCount * sizeof( uint64_t ) ) );
}
}
// HashSet (pairSet): raw items at full capacity, probe order depends on it
static void b2SerHashSet( b2RecBuffer* buf, const b2HashSet* hs )
{
b2SnapW_U32( buf, hs->capacity );
b2SnapW_U32( buf, hs->count );
if ( hs->capacity > 0 )
{
b2SnapW_Bytes( buf, hs->items, (int)( hs->capacity * sizeof( b2SetItem ) ) );
}
}
static void b2DesHashSet( b2SnapReader* r, b2HashSet* hs )
{
uint32_t cap = b2SnapR_U32( r );
uint32_t cnt = b2SnapR_U32( r );
// Probing masks with capacity-1, so capacity must be a power of two and count can't exceed it.
// (cap & (cap-1)) == 0 also accepts 0, which the empty branch handles.
bool valid =
b2SnapCheckCount( r, (int)cap, (int)sizeof( b2SetItem ), (int)sizeof( b2SetItem ) ) &&
( cap & ( cap - 1 ) ) == 0 && cnt <= cap;
if ( r->ok && valid == false )
{
r->ok = false;
}
// Destroy the fresh empty set the shell gave us
b2DestroySet( hs );
if ( !r->ok )
{
return;
}
if ( cap > 0 )
{
hs->items = b2Alloc( cap * sizeof( b2SetItem ) );
hs->capacity = cap;
hs->count = cnt;
b2SnapR_Bytes( r, hs->items, (int)( cap * sizeof( b2SetItem ) ) );
}
else
{
hs->items = NULL;
hs->capacity = 0;
hs->count = 0;
}
}
static void b2SerTree( b2RecBuffer* buf, const b2DynamicTree* tree )
{
b2SnapW_I32( buf, tree->nodeEnd );
b2SnapW_I32( buf, tree->nodeCapacity );
b2SnapW_I32( buf, tree->pairFreeList );
b2SnapW_I32( buf, tree->proxyCount );
b2SnapW_I32( buf, tree->proxyCapacity );
b2SnapW_I32( buf, tree->proxyFreeList );
b2SnapW_I32( buf, tree->dfsOrdered ? 1 : 0 );
b2SnapW_Bytes( buf, tree->nodes, tree->nodeEnd * (int)sizeof( b2TreeNode ) );
b2SnapW_Bytes( buf, tree->parents, tree->nodeEnd * (int)sizeof( int32_t ) );
b2SnapW_Bytes( buf, tree->proxies, tree->proxyCapacity * (int)sizeof( b2TreeProxy ) );
}
static void b2DesTree( b2SnapReader* r, b2DynamicTree* tree )
{
int nodeEnd = b2SnapR_I32( r );
int nodeCapacity = b2SnapR_I32( r );
int pairFreeList = b2SnapR_I32( r );
int proxyCount = b2SnapR_I32( r );
int proxyCapacity = b2SnapR_I32( r );
int proxyFreeList = b2SnapR_I32( r );
int ordered = b2SnapR_I32( r );
if ( r->ok && ( nodeEnd < 2 || ( nodeEnd & 1 ) != 0 || nodeCapacity < nodeEnd ||
proxyCount < 0 || proxyCapacity < 1 || proxyCapacity < proxyCount ) )
{
r->ok = false;
}
// Stream bounds against what is actually written
if ( r->ok && b2SnapCheckCount( r, nodeEnd, (int)sizeof( b2TreeNode ),
(int)sizeof( b2TreeNode ) + (int)sizeof( int32_t ) ) == false )
{
r->ok = false;
}
if ( r->ok && b2SnapCheckCount( r, proxyCapacity, (int)sizeof( b2TreeProxy ),
(int)sizeof( b2TreeProxy ) ) == false )
{
r->ok = false;
}
// Free what the shell or a live world holds, rebuild scratch included. Destroy zeroes the
// struct, which covers every field the image does not carry.
b2DestroyDynamicTreeInternal( tree );
if ( !r->ok )
{
// Leave a valid empty tree so the shell can still be destroyed
b2CreateDynamicTreeInternal( tree, 0 );
return;
}
// This is just an allocation hint and the exact value doesn't matter.
nodeCapacity = b2MinInt( nodeCapacity, 2 * nodeEnd );
tree->nodeEnd = nodeEnd;
tree->nodeCapacity = nodeCapacity;
tree->pairFreeList = pairFreeList;
tree->proxyCount = proxyCount;
tree->proxyCapacity = proxyCapacity;
tree->proxyFreeList = proxyFreeList;
tree->dfsOrdered = ordered != 0;
tree->nodes = b2AllocZero( nodeCapacity * sizeof( b2TreeNode ) );
b2SnapR_Bytes( r, tree->nodes, nodeEnd * (int)sizeof( b2TreeNode ) );
tree->parents = b2AllocZero( nodeCapacity * sizeof( int32_t ) );
b2SnapR_Bytes( r, tree->parents, nodeEnd * (int)sizeof( int32_t ) );
tree->proxies = b2AllocZero( proxyCapacity * sizeof( b2TreeProxy ) );
b2SnapR_Bytes( r, tree->proxies, proxyCapacity * (int)sizeof( b2TreeProxy ) );
}
// Solver set: setIndex + 5 POD arrays
static void b2SerSolverSet( b2RecBuffer* buf, const b2SolverSet* set )
{
b2SnapW_I32( buf, set->setIndex );
b2SerPodArray( buf, set->bodySims );
b2SerPodArray( buf, set->bodyStates );
b2SerPodArray( buf, set->jointSims );
b2SerPodArray( buf, set->contactSims );
b2SerPodArray( buf, set->islandSims );
}
static void b2DesSolverSet( b2SnapReader* r, b2SolverSet* set )
{
set->setIndex = b2SnapR_I32( r );
b2DesPodArray( r, set->bodySims );
b2DesPodArray( r, set->bodyStates );
b2DesPodArray( r, set->jointSims );
b2DesPodArray( r, set->contactSims );
b2DesPodArray( r, set->islandSims );
}
// Graph color: bodySet + contactSims + jointSims (overflow color has no bodySet)
static void b2SerGraphColor( b2RecBuffer* buf, const b2GraphColor* color, bool isOverflow )
{
if ( !isOverflow )
{
b2SerBitSet( buf, &color->bodySet );
}
b2SerPodArray( buf, color->contactSims );
b2SerPodArray( buf, color->jointSims );
}
static void b2DesGraphColor( b2SnapReader* r, b2GraphColor* color, bool isOverflow )
{
if ( !isOverflow )
{
b2DesBitSet( r, &color->bodySet );
}
b2DesPodArray( r, color->contactSims );
b2DesPodArray( r, color->jointSims );
// Transient wideConstraints/overflowConstraints stay NULL, wideConstraintCount 0
}
// World scalar config (simulation settings only, no runtime/shell fields)
// Only simulation scalars belong here, never host or worker state (workerCount,
// scheduler, callbacks, user data). b2World_RestoreFromSnapshot relies on that so an in-place
// restore preserves the live world's wiring.
static void b2SerWorldConfig( b2RecBuffer* buf, const b2World* world )
{
b2SnapW_Bytes( buf, &world->gravity, sizeof( b2Vec2 ) );
b2SnapW_Bytes( buf, &world->hitEventThreshold, sizeof( float ) );
b2SnapW_Bytes( buf, &world->restitutionThreshold, sizeof( float ) );
b2SnapW_I32( buf, world->restitutionIterations );
b2SnapW_Bytes( buf, &world->maxLinearSpeed, sizeof( float ) );
b2SnapW_Bytes( buf, &world->contactSpeed, sizeof( float ) );
b2SnapW_Bytes( buf, &world->contactHertz, sizeof( float ) );
b2SnapW_Bytes( buf, &world->contactDampingRatio, sizeof( float ) );
b2SnapW_Bytes( buf, &world->contactRecycleDistance, sizeof( float ) );
b2SnapW_Bytes( buf, &world->stepIndex, sizeof( uint64_t ) );
b2SnapW_I32( buf, world->splitIslandId );
// Step scaling cached for the force/torque reporting getters, which run between steps
b2SnapW_Bytes( buf, &world->inv_h, sizeof( float ) );
b2SnapW_Bytes( buf, &world->inv_dt, sizeof( float ) );
// End-event double-buffer parity, so the first post-restore event query reads the right half
b2SnapW_I32( buf, world->endEventArrayIndex );
// maxCapacity (b2Capacity struct)
b2SnapW_Bytes( buf, &world->maxCapacity, sizeof( b2Capacity ) );
// bool flags packed as individual bytes for layout stability
uint8_t flags = 0;
flags |= world->enableSleep ? 0x01u : 0u;
flags |= world->enableWarmStarting ? 0x02u : 0u;
flags |= world->enableContinuous ? 0x08u : 0u;
flags |= world->enableRestitutionPropagation ? 0x10u : 0u;
b2RecBufAppend( buf, &flags, 1 );
}
static void b2DesWorldConfig( b2SnapReader* r, b2World* world )
{
b2SnapR_Bytes( r, &world->gravity, sizeof( b2Vec2 ) );
b2SnapR_Bytes( r, &world->hitEventThreshold, sizeof( float ) );
b2SnapR_Bytes( r, &world->restitutionThreshold, sizeof( float ) );
world->restitutionIterations = b2SnapR_I32( r );
b2SnapR_Bytes( r, &world->maxLinearSpeed, sizeof( float ) );
b2SnapR_Bytes( r, &world->contactSpeed, sizeof( float ) );
b2SnapR_Bytes( r, &world->contactHertz, sizeof( float ) );
b2SnapR_Bytes( r, &world->contactDampingRatio, sizeof( float ) );
b2SnapR_Bytes( r, &world->contactRecycleDistance, sizeof( float ) );
b2SnapR_Bytes( r, &world->stepIndex, sizeof( uint64_t ) );
world->splitIslandId = b2SnapR_I32( r );
b2SnapR_Bytes( r, &world->inv_h, sizeof( float ) );
b2SnapR_Bytes( r, &world->inv_dt, sizeof( float ) );
world->endEventArrayIndex = b2SnapR_I32( r );
b2SnapR_Bytes( r, &world->maxCapacity, sizeof( b2Capacity ) );
uint8_t flags = 0;
b2SnapR_Bytes( r, &flags, 1 );
world->enableSleep = ( flags & 0x01u ) != 0;
world->enableWarmStarting = ( flags & 0x02u ) != 0;
world->enableContinuous = ( flags & 0x08u ) != 0;
world->enableRestitutionPropagation = ( flags & 0x10u ) != 0;
}
void b2SerializeWorld( b2World* world, b2RecBuffer* buf )
{
// Image header
b2SnapHeader hdr;
hdr.magic = B2_SNAP_MAGIC;
hdr.version = B2_SNAP_VERSION;
hdr.layoutHash = b2ComputeLayoutHash();
hdr.flags = B2_ENABLE_VALIDATION ? B2_SNAP_FLAG_VALIDATION : 0u;
if ( b2IsDoublePrecision() )
{
hdr.flags |= B2_SNAP_FLAG_DOUBLE_PRECISION;
}
b2SnapW_Bytes( buf, &hdr, (int)sizeof( hdr ) );
// World config
b2SerWorldConfig( buf, world );
// 7 id pools
b2SerIdPool( buf, &world->bodyIdPool );
b2SerIdPool( buf, &world->shapeIdPool );
b2SerIdPool( buf, &world->chainIdPool );
b2SerIdPool( buf, &world->contactIdPool );
b2SerIdPool( buf, &world->jointIdPool );
b2SerIdPool( buf, &world->islandIdPool );
b2SerIdPool( buf, &world->solverSetIdPool );
// Solver sets
int setCount = world->solverSets.count;
b2SnapW_I32( buf, setCount );
for ( int i = 0; i < setCount; ++i )
{
b2SerSolverSet( buf, world->solverSets.data + i );
}
// Sparse arrays. Bodies, shapes and joints carry a host userData pointer scrubbed to NULL on
// write. Contacts have no userData, so they go out as raw POD.
b2SerSimArray( buf, world->bodies, b2Body );
b2SerSimArray( buf, world->shapes, b2Shape );
b2SerPodArray( buf, world->fatAABBs );
b2SerPodArray( buf, world->contacts );
b2SerSimArray( buf, world->joints, b2Joint );
// Chain shapes: POD scalars then per-live-slot heap arrays
int chainCount = world->chainShapes.count;
b2SnapW_I32( buf, chainCount );
for ( int i = 0; i < chainCount; ++i )
{
b2ChainShape* chain = world->chainShapes.data + i;
// Write POD scalars
b2SnapW_I32( buf, chain->id );
b2SnapW_I32( buf, chain->bodyId );
b2SnapW_I32( buf, chain->nextChainId );
b2SnapW_I32( buf, chain->segmentCount );
b2SnapW_Bytes( buf, &chain->generation, sizeof( uint16_t ) );
if ( chain->id != B2_NULL_INDEX )
{
// Live slot: write the two heap arrays
b2SnapW_Bytes( buf, chain->shapeIndices, chain->segmentCount * (int)sizeof( int ) );
}
}
// Sensors: shapeId + 3 visitor arrays per slot
int sensorCount = world->sensors.count;
b2SnapW_I32( buf, sensorCount );
for ( int i = 0; i < sensorCount; ++i )
{
b2Sensor* s = world->sensors.data + i;
b2SnapW_I32( buf, s->shapeId );
b2SerPodArray( buf, s->hits );
b2SerPodArray( buf, s->overlaps1 );
b2SerPodArray( buf, s->overlaps2 );
}
// Islands: POD scalars + 3 inner arrays per slot
int islandCount = world->islands.count;
b2SnapW_I32( buf, islandCount );
for ( int i = 0; i < islandCount; ++i )
{
b2Island* island = world->islands.data + i;
b2SnapW_I32( buf, island->setIndex );
b2SnapW_I32( buf, island->localIndex );
b2SnapW_I32( buf, island->islandId );
b2SnapW_I32( buf, island->constraintRemoveCount );
b2SerPodArray( buf, island->bodies );
b2SerPodArray( buf, island->contacts );
b2SerPodArray( buf, island->joints );
}
// Broad phase
b2BroadPhase* bp = &world->broadPhase;
for ( int t = 0; t < b2_bodyTypeCount; ++t )
{
b2SerTree( buf, &bp->trees[t] );
}
b2SerHashSet( buf, &bp->pairSet );
// Constraint graph: B2_GRAPH_COLOR_COUNT colors
b2ConstraintGraph* graph = &world->constraintGraph;
for ( int c = 0; c < B2_GRAPH_COLOR_COUNT; ++c )
{
b2SerGraphColor( buf, &graph->colors[c], c == B2_OVERFLOW_INDEX );
}
}
// Free per-object heap the overwrite steps below don't reach, so restoring over a
// populated world doesn't leak. Outer arrays stay alive for the steps to reuse.
// Mirrors the per-object teardown in b2DestroyWorld.
static void b2FreeLiveSimElements( b2World* world )
{
for ( int i = 0; i < world->chainShapes.count; ++i )
{
b2ChainShape* chain = world->chainShapes.data + i;
if ( chain->id != B2_NULL_INDEX )
{
b2FreeChainData( chain );
}
}
for ( int i = 0; i < world->sensors.count; ++i )
{
b2Sensor* sensor = world->sensors.data + i;
b2Array_Destroy( sensor->hits );
b2Array_Destroy( sensor->overlaps1 );
b2Array_Destroy( sensor->overlaps2 );
}
for ( int i = 0; i < world->islands.count; ++i )
{
b2Island* island = world->islands.data + i;
b2Array_Destroy( island->bodies );
b2Array_Destroy( island->contacts );
b2Array_Destroy( island->joints );
}
}
// Overwrite a world with the simulation state from the reader. Mirrors the write
// order in b2SerializeWorld. The world must be a clean shell from b2CreateWorld, or
// a live world whose per-object heap was first freed by b2FreeLiveSimElements. Host
// wiring (scheduler, callbacks, user data) is never touched. Returns false on a
// corrupt image.
static bool b2DeserializeIntoShell( b2SnapReader* r, b2World* world )
{
// Step 1: world scalars
b2DesWorldConfig( r, world );
// Step 2: 7 id pools (overwrite entirely, including solverSetIdPool from the 3 pre-created
// sets)
b2DesIdPool( r, &world->bodyIdPool );
b2DesIdPool( r, &world->shapeIdPool );
b2DesIdPool( r, &world->chainIdPool );
b2DesIdPool( r, &world->contactIdPool );
b2DesIdPool( r, &world->jointIdPool );
b2DesIdPool( r, &world->islandIdPool );
b2DesIdPool( r, &world->solverSetIdPool );
// Step 3: solver sets
// (a) Destroy the 5 inner arrays of the 3 pre-created sets WITHOUT freeing the set id
// (b2DestroySolverSet would do that, but we already authored the id pool above)
for ( int i = 0; i < world->solverSets.count; ++i )
{
b2SolverSet* set = world->solverSets.data + i;
b2Array_Destroy( set->bodySims );
b2Array_Destroy( set->bodyStates );
b2Array_Destroy( set->contactSims );
b2Array_Destroy( set->jointSims );
b2Array_Destroy( set->islandSims );
}
// Each set writes at least setIndex plus 5 array counts
int setCount = b2SnapR_I32( r );
if ( r->ok && b2SnapCheckCount( r, setCount, (int)sizeof( b2SolverSet ),
6 * (int)sizeof( int ) ) == false )
{
r->ok = false;
}
if ( r->ok )
{
// (b) Resize outer array to hold all sets, zeroing new slots so their headers are clean
b2Array_ResizeAndSetZero( world->solverSets, setCount );
// (c) Deserialize each set
for ( int i = 0; i < setCount; ++i )
{
b2DesSolverSet( r, world->solverSets.data + i );
}
}
// Step 4: sparse arrays. userData was written as NULL by the serializer, so a plain copy
// restores it cleanly with no host pointers to scrub here.
b2DesPodArray( r, world->bodies );
b2DesPodArray( r, world->shapes );
b2DesPodArray( r, world->fatAABBs );
b2DesPodArray( r, world->contacts );
b2DesPodArray( r, world->joints );
// Step 5: chain shapes
{
// Destroy the shell's chainShapes array (empty, but has a backing allocation)
b2Array_Destroy( world->chainShapes );
b2Array_Create( world->chainShapes );
// Each chain writes 4 ints plus a uint16 generation
int chainCount = b2SnapR_I32( r );
if ( r->ok &&
b2SnapCheckCount( r, chainCount, (int)sizeof( b2ChainShape ),
4 * (int)sizeof( int ) + (int)sizeof( uint16_t ) ) == false )
{
r->ok = false;
}
if ( r->ok )
{
b2Array_Resize( world->chainShapes, chainCount );
// Zero the whole array so free slots have NULL pointers
memset( world->chainShapes.data, 0, chainCount * sizeof( b2ChainShape ) );
}
for ( int i = 0; i < chainCount && r->ok; ++i )
{
b2ChainShape* chain = world->chainShapes.data + i;
chain->id = b2SnapR_I32( r );
chain->bodyId = b2SnapR_I32( r );
chain->nextChainId = b2SnapR_I32( r );
chain->segmentCount = b2SnapR_I32( r );
b2SnapR_Bytes( r, &chain->generation, sizeof( uint16_t ) );
// A partial read leaves id as 0, which is a valid slot value, so gate the live branch
// on r->ok
if ( r->ok && chain->id != B2_NULL_INDEX )
{
if ( b2SnapCheckCount( r, chain->segmentCount, (int)sizeof( int ),
(int)sizeof( int ) ) == false )
{
r->ok = false;
break;
}
// Live slot: allocate and copy heap arrays
chain->shapeIndices = b2Alloc( chain->segmentCount * (int)sizeof( int ) );
b2SnapR_Bytes( r, chain->shapeIndices, chain->segmentCount * (int)sizeof( int ) );
}
else
{
// Free slot must have NULL pointers; zero init above handles this
chain->shapeIndices = NULL;
}
}
}
// Step 6: sensors
{
// Destroy the shell's sensors array
b2Array_Destroy( world->sensors );
b2Array_Create( world->sensors );
// Each sensor writes shapeId plus 3 array counts
int sensorCount = b2SnapR_I32( r );
if ( r->ok && b2SnapCheckCount( r, sensorCount, (int)sizeof( b2Sensor ),
4 * (int)sizeof( int ) ) == false )
{
r->ok = false;
}
if ( r->ok )
{
b2Array_Resize( world->sensors, sensorCount );
// Zero so inner array headers start clean
memset( world->sensors.data, 0, sensorCount * sizeof( b2Sensor ) );
}
for ( int i = 0; i < sensorCount && r->ok; ++i )
{
b2Sensor* s = world->sensors.data + i;
s->shapeId = b2SnapR_I32( r );
// Re-init inner arrays then fill them
b2Array_Create( s->hits );
b2Array_Create( s->overlaps1 );
b2Array_Create( s->overlaps2 );
b2DesPodArray( r, s->hits );
b2DesPodArray( r, s->overlaps1 );
b2DesPodArray( r, s->overlaps2 );
}
}
// Step 7: islands
{
// Destroy the shell's islands array
b2Array_Destroy( world->islands );
b2Array_Create( world->islands );
// Each island writes 4 ints plus 3 array counts
int islandCount = b2SnapR_I32( r );
if ( r->ok && b2SnapCheckCount( r, islandCount, (int)sizeof( b2Island ),
7 * (int)sizeof( int ) ) == false )
{
r->ok = false;
}
if ( r->ok )
{
b2Array_Resize( world->islands, islandCount );
memset( world->islands.data, 0, islandCount * sizeof( b2Island ) );
}
for ( int i = 0; i < islandCount && r->ok; ++i )
{
b2Island* island = world->islands.data + i;
island->setIndex = b2SnapR_I32( r );
island->localIndex = b2SnapR_I32( r );
island->islandId = b2SnapR_I32( r );
island->constraintRemoveCount = b2SnapR_I32( r );
b2Array_Create( island->bodies );
b2Array_Create( island->contacts );
b2Array_Create( island->joints );
b2DesPodArray( r, island->bodies );
b2DesPodArray( r, island->contacts );
b2DesPodArray( r, island->joints );
}
}
// Step 8: broad phase
{
b2BroadPhase* bp = &world->broadPhase;
// Trees: the shell already allocated tree nodes; free them and replace
for ( int t = 0; t < b2_bodyTypeCount; ++t )
{
b2DesTree( r, &bp->trees[t] );
}
// pairSet
b2DesHashSet( r, &bp->pairSet );
}
// Step 9: constraint graph
{
b2ConstraintGraph* graph = &world->constraintGraph;
for ( int c = 0; c < B2_GRAPH_COLOR_COUNT; ++c )
{
b2DesGraphColor( r, &graph->colors[c], c == B2_OVERFLOW_INDEX );
}
}
// Event buffers are transient and never serialized. An in-place restore reuses the live
// world's arrays, so end events queued by a between-step mutator (b2Body_Disable and friends)
// would survive the restore and surface after the first resimmed step. Reset to match a fresh
// world from snapshot. The double-buffer parity is restored, but the contents must start empty.
b2Array_Clear( world->bodyMoveEvents );
b2Array_Clear( world->sensorBeginEvents );
b2Array_Clear( world->sensorEndEvents[0] );
b2Array_Clear( world->sensorEndEvents[1] );
b2Array_Clear( world->contactBeginEvents );
b2Array_Clear( world->contactEndEvents[0] );
b2Array_Clear( world->contactEndEvents[1] );
b2Array_Clear( world->contactHitEvents );
b2Array_Clear( world->jointEvents );
return r->ok;
}
// Validate a snapshot image header and arm the reader just past it. Returns false on a rejected
// image (too small, bad magic, wrong version, layout mismatch), leaving the caller's world
// untouched. A layout mismatch caused purely by differing validation builds is called out, since
// the validation gated fields change struct sizes and such images can never share a layout.
static bool b2OpenSnapshotImage( const uint8_t* image, int size, b2SnapReader* r )
{
if ( image == NULL || size < (int)sizeof( b2SnapHeader ) )
{
return false;
}
b2SnapHeader hdr;
memcpy( &hdr, image, sizeof( hdr ) );
if ( hdr.magic != B2_SNAP_MAGIC || hdr.version != B2_SNAP_VERSION )
{
return false;
}
// World positions are stored at the build precision, so the layout differs irreconcilably
// across modes. Called out before the layout hash so the cause is clear rather than a generic
// mismatch.
bool imageDouble = ( hdr.flags & B2_SNAP_FLAG_DOUBLE_PRECISION ) != 0;
bool buildDouble = b2IsDoublePrecision();
if ( imageDouble != buildDouble )
{
b2Log( "snapshot precision mismatch: image %s, this build %s\n",
imageDouble ? "double" : "float", buildDouble ? "double" : "float" );
return false;
}
if ( hdr.layoutHash != b2ComputeLayoutHash() )
{
bool imageValidation = ( hdr.flags & B2_SNAP_FLAG_VALIDATION ) != 0;
if ( imageValidation != (bool)B2_ENABLE_VALIDATION )
{
b2Log( "snapshot layout mismatch: image built with validation %s, this build %s\n",
imageValidation ? "on" : "off", B2_ENABLE_VALIDATION ? "on" : "off" );
}
else
{
b2Log( "snapshot layout mismatch\n" );
}
return false;
}
r->data = image;
r->cursor = (int)sizeof( hdr );
r->size = size;
r->ok = true;
return true;
}
b2WorldId b2CreateWorldFromSnapshot( const uint8_t* image, int size, int workerCount )
{
b2WorldId nullId = b2_nullWorldId;
b2SnapReader readerStorage;
b2SnapReader* r = &readerStorage;
if ( b2OpenSnapshotImage( image, size, r ) == false )
{
return nullId;
}
// Build a minimal valid def so b2CreateWorld produces a fully valid shell
b2WorldDef def = b2DefaultWorldDef();
def.workerCount = workerCount;
def.enqueueTask = NULL;
def.finishTask = NULL;
def.userTaskContext = NULL;
// Capacity is only a sizing hint. Every container is resized from the image below
b2WorldId id = b2CreateWorld( &def );
if ( !b2World_IsValid( id ) )
{
return nullId;
}
b2World* world = b2GetWorldFromId( id );
if ( b2DeserializeIntoShell( r, world ) == false )
{
// Image was corrupt; clean up by destroying the world
b2DestroyWorld( id );
return nullId;
}
// A world loaded from scratch carries no host pointers
world->preSolveFcn = NULL;
world->preSolveContext = NULL;
world->customFilterFcn = NULL;
world->customFilterContext = NULL;
world->userData = NULL;
return id;
}
bool b2World_RestoreFromSnapshot( b2WorldId worldId, const uint8_t* image, int size )
{
// Validate the image fully before touching the world so a bad image leaves it intact
b2SnapReader readerStorage;
b2SnapReader* r = &readerStorage;
if ( b2OpenSnapshotImage( image, size, r ) == false )
{
return false;
}
b2World* world = b2GetWorldFromId( worldId );
// Restoring mid step would corrupt an in-flight solve
B2_ASSERT( world->locked == false );
if ( world->locked )
{
return false;
}
if ( world->recording != NULL )
{
b2Log( "b2World_RestoreFromSnapshot: not supported while recording\n" );
return false;
}
// Point of no return. The slot, generation, and host wiring are kept, so held ids
// resolve into the rebuilt world. A truncated payload past here leaves the world
// unusable and the caller must destroy it.
b2FreeLiveSimElements( world );
return b2DeserializeIntoShell( r, world );
}
int b2World_GetSnapshot( b2WorldId worldId, uint8_t* image, int capacity )
{
b2World* world = b2GetWorldFromId( worldId );
// Serializing mid step would capture an inconsistent, in-flight world
B2_ASSERT( world->locked == false );
if ( world->locked )
{
return 0;
}
// Size query: count the bytes without allocating or copying the whole image
if ( image == NULL )
{
b2RecBuffer counter = { 0 };
counter.countOnly = true;
b2SerializeWorld( world, &counter );
return counter.size;
}
b2RecBuffer buf = { 0 };
b2SerializeWorld( world, &buf );
int size = buf.size;
if ( size <= capacity )
{
memcpy( image, buf.data, size );
}
b2RecBufFree( &buf );
return size;
}
static uint64_t b2FnvMixBytes( uint64_t hash, const void* data, int n )
{
const uint8_t* p = (const uint8_t*)data;
for ( int i = 0; i < n; ++i )
{
hash = ( hash ^ (uint64_t)p[i] ) * B2_SNAP_FNV_PRIME;
}
return hash;
}
static uint64_t b2FnvMixFloat( uint64_t hash, float f )
{
uint32_t bits;
memcpy( &bits, &f, 4 );
return ( hash ^ (uint64_t)bits ) * B2_SNAP_FNV_PRIME;
}
static uint64_t b2FnvMixInt( uint64_t hash, int v )
{
return ( hash ^ (uint64_t)(uint32_t)v ) * B2_SNAP_FNV_PRIME;
}
uint64_t b2HashWorldStateDeep( b2World* world )
{
uint64_t hash = B2_SNAP_FNV_INIT;
// Bodies: same iteration order as b2HashWorldState (sparse array, skip free slots)
int bodyCount = world->bodies.count;
for ( int i = 0; i < bodyCount; ++i )
{
b2Body* body = world->bodies.data + i;
if ( body->id != i )
{
continue;
}
b2BodySim* sim = b2GetBodySim( world, body );
hash = b2FnvMixPosition( hash, sim->transform.p );
hash = b2FnvMixFloat( hash, sim->transform.q.c );
hash = b2FnvMixFloat( hash, sim->transform.q.s );
b2BodyState* state = b2GetBodyState( world, body );
if ( state != NULL )
{
hash = b2FnvMixFloat( hash, state->linearVelocity.x );
hash = b2FnvMixFloat( hash, state->linearVelocity.y );
hash = b2FnvMixFloat( hash, state->angularVelocity );
}
// Index bookkeeping
hash = b2FnvMixInt( hash, body->setIndex );
hash = b2FnvMixInt( hash, body->localIndex );
}
// Contacts: sparse array, skip free slots (colorIndex == B2_NULL_INDEX and setIndex ==
// B2_NULL_INDEX for free)
int contactCount = world->contacts.count;
for ( int i = 0; i < contactCount; ++i )
{
b2Contact* contact = world->contacts.data + i;
if ( contact->contactId != i )
{
continue;
}
hash = b2FnvMixInt( hash, contact->setIndex );
hash = b2FnvMixInt( hash, contact->colorIndex );
hash = b2FnvMixInt( hash, contact->localIndex );
// Manifold points + impulses from the contact sim
// Exclude B2_ENABLE_VALIDATION-gated bodyIdA/bodyIdB for build-config stability
b2ContactSim* sim = b2GetContactSim( world, contact );
if ( sim != NULL )
{
b2Manifold* m = &sim->manifold;
hash = b2FnvMixInt( hash, m->pointCount );
for ( int p = 0; p < m->pointCount; ++p )
{
hash = b2FnvMixFloat( hash, m->points[p].normalImpulse );
hash = b2FnvMixFloat( hash, m->points[p].tangentImpulse );
hash = b2FnvMixFloat( hash, m->points[p].totalNormalImpulse );
}
}
}
// Joints: sparse array, skip free slots
int jointCount = world->joints.count;
for ( int i = 0; i < jointCount; ++i )
{
b2Joint* joint = world->joints.data + i;
if ( joint->jointId != i )
{
continue;
}
hash = b2FnvMixInt( hash, joint->setIndex );
hash = b2FnvMixInt( hash, joint->colorIndex );
hash = b2FnvMixInt( hash, joint->localIndex );
b2JointSim* sim = b2GetJointSim( world, joint );
if ( sim != NULL )
{
// Hash accumulated impulses per joint type
switch ( sim->type )
{
case b2_distanceJoint:
hash = b2FnvMixFloat( hash, sim->distanceJoint.impulse );
hash = b2FnvMixFloat( hash, sim->distanceJoint.lowerImpulse );
hash = b2FnvMixFloat( hash, sim->distanceJoint.upperImpulse );
hash = b2FnvMixFloat( hash, sim->distanceJoint.motorImpulse );
break;
case b2_moverJoint:
hash = b2FnvMixFloat( hash, sim->moverJoint.linearVelocityImpulse.x );
hash = b2FnvMixFloat( hash, sim->moverJoint.linearVelocityImpulse.y );
break;
case b2_motorJoint:
hash = b2FnvMixFloat( hash, sim->motorJoint.linearVelocityImpulse.x );
hash = b2FnvMixFloat( hash, sim->motorJoint.linearVelocityImpulse.y );
hash = b2FnvMixFloat( hash, sim->motorJoint.angularVelocityImpulse );
hash = b2FnvMixFloat( hash, sim->motorJoint.linearSpringImpulse.x );
hash = b2FnvMixFloat( hash, sim->motorJoint.linearSpringImpulse.y );
hash = b2FnvMixFloat( hash, sim->motorJoint.angularSpringImpulse );
break;
case b2_pogoJoint:
hash = b2FnvMixFloat( hash, sim->pogoJoint.impulse );
hash = b2FnvMixFloat( hash, sim->pogoJoint.velocity );
break;
case b2_prismaticJoint:
hash = b2FnvMixBytes( hash, &sim->prismaticJoint.impulse, sizeof( b2Vec2 ) );
hash = b2FnvMixFloat( hash, sim->prismaticJoint.springImpulse );
hash = b2FnvMixFloat( hash, sim->prismaticJoint.motorImpulse );
hash = b2FnvMixFloat( hash, sim->prismaticJoint.lowerImpulse );
hash = b2FnvMixFloat( hash, sim->prismaticJoint.upperImpulse );
break;
case b2_revoluteJoint:
hash =
b2FnvMixBytes( hash, &sim->revoluteJoint.linearImpulse, sizeof( b2Vec2 ) );
hash = b2FnvMixFloat( hash, sim->revoluteJoint.springImpulse );
hash = b2FnvMixFloat( hash, sim->revoluteJoint.motorImpulse );
hash = b2FnvMixFloat( hash, sim->revoluteJoint.lowerImpulse );
hash = b2FnvMixFloat( hash, sim->revoluteJoint.upperImpulse );
break;
case b2_weldJoint:
hash = b2FnvMixBytes( hash, &sim->weldJoint.linearImpulse, sizeof( b2Vec2 ) );
hash = b2FnvMixFloat( hash, sim->weldJoint.angularImpulse );
break;
case b2_wheelJoint:
hash = b2FnvMixFloat( hash, sim->wheelJoint.perpImpulse );
hash = b2FnvMixFloat( hash, sim->wheelJoint.motorImpulse );
hash = b2FnvMixFloat( hash, sim->wheelJoint.springImpulse );
hash = b2FnvMixFloat( hash, sim->wheelJoint.lowerImpulse );
hash = b2FnvMixFloat( hash, sim->wheelJoint.upperImpulse );
break;
default:
break;
}
}
}
// 7 id pools: nextIndex + count
hash = b2FnvMixInt( hash, world->bodyIdPool.nextIndex );
hash = b2FnvMixInt( hash, b2GetIdCount( &world->bodyIdPool ) );
hash = b2FnvMixInt( hash, world->shapeIdPool.nextIndex );
hash = b2FnvMixInt( hash, b2GetIdCount( &world->shapeIdPool ) );
hash = b2FnvMixInt( hash, world->chainIdPool.nextIndex );
hash = b2FnvMixInt( hash, b2GetIdCount( &world->chainIdPool ) );
hash = b2FnvMixInt( hash, world->contactIdPool.nextIndex );
hash = b2FnvMixInt( hash, b2GetIdCount( &world->contactIdPool ) );
hash = b2FnvMixInt( hash, world->jointIdPool.nextIndex );
hash = b2FnvMixInt( hash, b2GetIdCount( &world->jointIdPool ) );
hash = b2FnvMixInt( hash, world->islandIdPool.nextIndex );
hash = b2FnvMixInt( hash, b2GetIdCount( &world->islandIdPool ) );
hash = b2FnvMixInt( hash, world->solverSetIdPool.nextIndex );
hash = b2FnvMixInt( hash, b2GetIdCount( &world->solverSetIdPool ) );
// Solver set count
hash = b2FnvMixInt( hash, world->solverSets.count );
return hash;
}
uint64_t b2World_GetStateHash( b2WorldId worldId )
{
b2World* world = b2GetWorldFromId( worldId );
return b2HashWorldStateDeep( world );
}