packages feed

Box2D-0.1.0.0: upstream/src/physics_world.c

// SPDX-FileCopyrightText: 2026 Erin Catto
// SPDX-License-Identifier: MIT

#if defined( _MSC_VER ) && !defined( _CRT_SECURE_NO_WARNINGS )
#define _CRT_SECURE_NO_WARNINGS
#endif

#include "physics_world.h"

#include "aabb.h"
#include "arena_allocator.h"
#include "bitset.h"
#include "body.h"
#include "broad_phase.h"
#include "constraint_graph.h"
#include "contact.h"
#include "core.h"
#include "dynamic_tree.h"
#include "island.h"
#include "joint.h"
#include "parallel_for.h"
#include "platform.h"
#include "recording.h"
#include "scheduler.h"
#include "sensor.h"
#include "shape.h"
#include "simd.h"
#include "solver.h"
#include "solver_set.h"

#include "box2d/box2d.h"
#include "box2d/constants.h"

#include <float.h>
#include <stdio.h>
#include <string.h>

_Static_assert( B2_MAX_WORLDS > 0, "must be 1 or more" );
_Static_assert( B2_MAX_WORLDS < UINT16_MAX, "B2_MAX_WORLDS limit exceeded" );
static b2World b2_worlds[B2_MAX_WORLDS];

static b2World* b2GetUnlockedWorldFromId( b2WorldId id )
{
	B2_ASSERT( 1 <= id.index1 && id.index1 <= B2_MAX_WORLDS );
	b2World* world = b2_worlds + ( id.index1 - 1 );
	B2_ASSERT( id.index1 == world->worldId + 1 );
	B2_ASSERT( id.generation == world->generation );

	// A world accessed from an id should not be locked
	if ( world->locked )
	{
		B2_ASSERT( false );
		return NULL;
	}
	return world;
}

b2World* b2GetWorldFromId( b2WorldId id )
{
	B2_ASSERT( 1 <= id.index1 && id.index1 <= B2_MAX_WORLDS );
	b2World* world = b2_worlds + ( id.index1 - 1 );
	B2_ASSERT( id.index1 == world->worldId + 1 );
	B2_ASSERT( id.generation == world->generation );
	return world;
}

b2World* b2GetWorld( int index )
{
	B2_ASSERT( 0 <= index && index < B2_MAX_WORLDS );
	b2World* world = b2_worlds + index;
	B2_ASSERT( world->worldId == index );
	return world;
}

b2World* b2GetWorldLocked( int index )
{
	B2_ASSERT( 0 <= index && index < B2_MAX_WORLDS );
	b2World* world = b2_worlds + index;
	B2_ASSERT( world->worldId == index );
	if ( world->locked )
	{
		B2_ASSERT( false );
		return NULL;
	}

	return world;
}

static void* b2DefaultAddTaskFcn( b2TaskCallbackFcn* task, void* taskContext, void* userContext )
{
	B2_UNUSED( userContext );
	task( taskContext );
	return NULL;
}

static void b2DefaultFinishTaskFcn( void* userTask, void* userContext )
{
	B2_UNUSED( userTask, userContext );
}

static void b2CreateWorkerContexts( b2World* world )
{
	b2Array_Create( world->taskContexts );
	b2Array_ResizeAndSetZero( world->taskContexts, world->workerCount );

	b2Array_Create( world->sensorTaskContexts );
	b2Array_ResizeAndSetZero( world->sensorTaskContexts, world->workerCount );

	for ( int i = 0; i < world->workerCount; ++i )
	{
		b2Array_CreateN( world->taskContexts.data[i].sensorHits, 8 );
		b2Array_Create( world->taskContexts.data[i].pairKeys );
		world->taskContexts.data[i].contactStateBitSet = b2CreateBitSet( 1024 );
		world->taskContexts.data[i].hitEventBitSet = b2CreateBitSet( 1024 );
		world->taskContexts.data[i].hasHitEvents = false;
		world->taskContexts.data[i].jointStateBitSet = b2CreateBitSet( 1024 );
		world->taskContexts.data[i].enlargedSimBitSet = b2CreateBitSet( 256 );
		world->taskContexts.data[i].awakeIslandBitSet = b2CreateBitSet( 256 );
		world->taskContexts.data[i].splitIslandId = B2_NULL_INDEX;

		world->sensorTaskContexts.data[i].eventBits = b2CreateBitSet( 128 );
	}
}

static void b2DestroyWorkerContexts( b2World* world )
{
	for ( int i = 0; i < world->workerCount; ++i )
	{
		b2Array_Destroy( world->taskContexts.data[i].sensorHits );
		b2Array_Destroy( world->taskContexts.data[i].pairKeys );
		b2DestroyBitSet( &world->taskContexts.data[i].contactStateBitSet );
		b2DestroyBitSet( &world->taskContexts.data[i].hitEventBitSet );
		b2DestroyBitSet( &world->taskContexts.data[i].jointStateBitSet );
		b2DestroyBitSet( &world->taskContexts.data[i].enlargedSimBitSet );
		b2DestroyBitSet( &world->taskContexts.data[i].awakeIslandBitSet );

		b2DestroyBitSet( &world->sensorTaskContexts.data[i].eventBits );
	}

	b2Array_Destroy( world->taskContexts );
	b2Array_Destroy( world->sensorTaskContexts );
}

b2WorldId b2CreateWorld( const b2WorldDef* def )
{
	_Static_assert( B2_MAX_WORLDS < UINT16_MAX, "B2_MAX_WORLDS limit exceeded" );
	B2_CHECK_DEF( def );
	B2_CHECK_INPUT_RETURN( b2IsValidVec2( def->gravity ), (b2WorldId){ 0 } );
	B2_CHECK_INPUT_RETURN( b2IsValidFloat( def->restitutionThreshold ), (b2WorldId){ 0 } );
	B2_CHECK_INPUT_RETURN( def->restitutionIterations >= 0, (b2WorldId){ 0 } );
	B2_CHECK_INPUT_RETURN( b2IsValidFloat( def->hitEventThreshold ), (b2WorldId){ 0 } );
	B2_CHECK_INPUT_RETURN( b2IsValidFloat( def->contactHertz ), (b2WorldId){ 0 } );
	B2_CHECK_INPUT_RETURN( b2IsValidFloat( def->contactDampingRatio ), (b2WorldId){ 0 } );
	B2_CHECK_INPUT_RETURN( b2IsValidFloat( def->contactSpeed ), (b2WorldId){ 0 } );
	B2_CHECK_INPUT_RETURN( b2IsValidFloat( def->maximumLinearSpeed ) && def->maximumLinearSpeed > 0.0f, (b2WorldId){ 0 } );

	if ( b2IsDenormalFlushEnabled() )
	{
		b2Log( "Denormal flushing is enabled, determinism not supported" );
	}

	int worldId = B2_NULL_INDEX;
	for ( int i = 0; i < B2_MAX_WORLDS; ++i )
	{
		if ( b2_worlds[i].inUse == false )
		{
			worldId = i;
			break;
		}
	}

	if ( worldId == B2_NULL_INDEX )
	{
		return (b2WorldId){ 0 };
	}

	b2World* world = b2_worlds + worldId;
	uint16_t generation = world->generation;

	*world = (b2World){ 0 };

	world->worldId = (uint16_t)worldId;
	world->generation = generation;
	world->inUse = true;

	world->stack = b2CreateStack( b2MaxInt( 2048, def->capacity.arenaByteCount ) );
	b2CreateBroadPhase( &world->broadPhase, &def->capacity );
	b2CreateGraph( &world->constraintGraph, &def->capacity );

	// pools
	world->bodyIdPool = b2CreateIdPool();

	int bodyCapacity = b2MaxInt( 16, def->capacity.staticBodyCount + def->capacity.dynamicBodyCount );
	b2Array_CreateN( world->bodies, bodyCapacity );
	b2Array_CreateN( world->solverSets, 8 );

	// add empty static, active, and disabled body sets
	world->solverSetIdPool = b2CreateIdPool();
	b2SolverSet set = { 0 };

	// static set
	set.setIndex = b2AllocId( &world->solverSetIdPool );
	b2Array_Push( world->solverSets, set );
	b2Array_Reserve( world->solverSets.data[b2_staticSet].bodySims, b2MaxInt( 16, def->capacity.staticBodyCount ) );
	B2_ASSERT( world->solverSets.data[b2_staticSet].setIndex == b2_staticSet );

	// disabled set
	set.setIndex = b2AllocId( &world->solverSetIdPool );
	b2Array_Push( world->solverSets, set );
	B2_ASSERT( world->solverSets.data[b2_disabledSet].setIndex == b2_disabledSet );

	// awake set
	set.setIndex = b2AllocId( &world->solverSetIdPool );
	b2Array_Push( world->solverSets, set );
	b2Array_Reserve( world->solverSets.data[b2_awakeSet].bodySims, b2MaxInt( 16, def->capacity.dynamicBodyCount ) );
	b2Array_Reserve( world->solverSets.data[b2_awakeSet].bodyStates, b2MaxInt( 16, def->capacity.dynamicBodyCount ) );
	b2Array_Reserve( world->solverSets.data[b2_awakeSet].contactSims, b2MaxInt( 16, def->capacity.contactCount ) );
	B2_ASSERT( world->solverSets.data[b2_awakeSet].setIndex == b2_awakeSet );

	world->shapeIdPool = b2CreateIdPool();

	int shapeCapacity = b2MaxInt( 16, def->capacity.staticShapeCount + def->capacity.dynamicShapeCount );
	b2Array_CreateN( world->shapes, shapeCapacity );
	b2Array_CreateN( world->fatAABBs, shapeCapacity );

	world->chainIdPool = b2CreateIdPool();
	b2Array_CreateN( world->chainShapes, 4 );

	world->contactIdPool = b2CreateIdPool();
	b2Array_CreateN( world->contacts, b2MaxInt( 16, def->capacity.contactCount ) );

	world->jointIdPool = b2CreateIdPool();
	b2Array_CreateN( world->joints, 16 );

	world->islandIdPool = b2CreateIdPool();
	b2Array_CreateN( world->islands, b2MaxInt( 16, def->capacity.dynamicBodyCount ) );

	b2Array_CreateN( world->sensors, 4 );

	b2Array_CreateN( world->bodyMoveEvents, 4 );
	b2Array_CreateN( world->sensorBeginEvents, 4 );
	b2Array_CreateN( world->sensorEndEvents[0], 4 );
	b2Array_CreateN( world->sensorEndEvents[1], 4 );
	b2Array_CreateN( world->contactBeginEvents, 4 );
	b2Array_CreateN( world->contactEndEvents[0], 4 );
	b2Array_CreateN( world->contactEndEvents[1], 4 );
	b2Array_CreateN( world->contactHitEvents, 4 );
	b2Array_CreateN( world->jointEvents, 4 );
	world->endEventArrayIndex = 0;

	world->stepIndex = 0;
	world->splitIslandId = B2_NULL_INDEX;
	world->activeTaskCount = 0;
	world->taskCount = 0;
	world->gravity = def->gravity;
	world->hitEventThreshold = b2ClampFloat( def->hitEventThreshold, 0.0f, FLT_MAX );
	world->restitutionThreshold = b2ClampFloat( def->restitutionThreshold, 0.0f, FLT_MAX );
	// Clamp this to avoid overflowing the solver sync flags.
	world->restitutionIterations = b2ClampInt( def->restitutionIterations, 0, B2_MAX_RESTITUTION_ITERATIONS );
	world->enableRestitutionPropagation = def->enableRestitutionPropagation;
	world->maxLinearSpeed = def->maximumLinearSpeed;
	world->contactSpeed = b2ClampFloat( def->contactSpeed, 0.0f, FLT_MAX );
	world->contactHertz = b2ClampFloat( def->contactHertz, 0.0f, FLT_MAX );
	world->contactDampingRatio = b2ClampFloat( def->contactDampingRatio, 0.0f, FLT_MAX );
	world->contactRecycleDistance = B2_CONTACT_RECYCLE_DISTANCE;

	if ( def->frictionCallback != NULL )
	{
		world->frictionCallback = def->frictionCallback;
	}

	if ( def->restitutionCallback != NULL )
	{
		world->restitutionCallback = def->restitutionCallback;
	}

	world->enableSleep = def->enableSleep;
	world->locked = false;
	world->enableWarmStarting = true;
	world->enableContinuous = def->enableContinuous;
	world->simdWidth = b2GetSIMDWidth();
	world->userTreeTask = NULL;
	world->userData = def->userData;

	if ( def->workerCount > 0 && def->enqueueTask != NULL && def->finishTask != NULL )
	{
		// External task system
		world->workerCount = b2MinInt( def->workerCount, B2_MAX_WORKERS );
		world->enqueueTaskFcn = def->enqueueTask;
		world->finishTaskFcn = def->finishTask;
		world->userTaskContext = def->userTaskContext;
		world->scheduler = NULL;
	}
	else if ( def->workerCount > 1 )
	{
		// Built-in scheduler
		world->workerCount = b2MinInt( def->workerCount, B2_MAX_WORKERS );
		world->scheduler = b2CreateScheduler( world->workerCount );
		world->enqueueTaskFcn = b2SchedulerEnqueueTask;
		world->finishTaskFcn = b2SchedulerFinishTask;
		world->userTaskContext = world->scheduler;
	}
	else
	{
		// Serial fallback
		world->workerCount = 1;
		world->enqueueTaskFcn = b2DefaultAddTaskFcn;
		world->finishTaskFcn = b2DefaultFinishTaskFcn;
		world->userTaskContext = NULL;
		world->scheduler = NULL;
	}

	b2CreateWorkerContexts( world );

	world->debugBodySet = b2CreateBitSet( 256 );
	world->debugJointSet = b2CreateBitSet( 256 );
	world->debugContactSet = b2CreateBitSet( 256 );
	world->debugIslandSet = b2CreateBitSet( 256 );

	// Recording is started by the host with b2World_StartRecording, never from the world def
	world->recording = NULL;

	// add one to worldId so that 0 represents a null b2WorldId
	return (b2WorldId){
		.index1 = (uint16_t)( worldId + 1 ),
		.generation = world->generation,
	};
}

void b2DestroyWorld( b2WorldId worldId )
{
	b2World* world = b2GetWorldFromId( worldId );

	// Detach any recording before teardown. The user owns and frees the recording buffer.
	b2StopRecordingInternal( world );

	if ( world->scheduler != NULL )
	{
		b2DestroyScheduler( world->scheduler );
		world->scheduler = NULL;
	}

	b2DestroyBitSet( &world->debugBodySet );
	b2DestroyBitSet( &world->debugJointSet );
	b2DestroyBitSet( &world->debugContactSet );
	b2DestroyBitSet( &world->debugIslandSet );

	b2DestroyWorkerContexts( world );

	b2Array_Destroy( world->bodyMoveEvents );
	b2Array_Destroy( world->sensorBeginEvents );
	b2Array_Destroy( world->sensorEndEvents[0] );
	b2Array_Destroy( world->sensorEndEvents[1] );
	b2Array_Destroy( world->contactBeginEvents );
	b2Array_Destroy( world->contactEndEvents[0] );
	b2Array_Destroy( world->contactEndEvents[1] );
	b2Array_Destroy( world->contactHitEvents );
	b2Array_Destroy( world->jointEvents );

	int chainCapacity = world->chainShapes.count;
	for ( int i = 0; i < chainCapacity; ++i )
	{
		b2ChainShape* chain = world->chainShapes.data + i;
		if ( chain->id != B2_NULL_INDEX )
		{
			b2FreeChainData( chain );
		}
		else
		{
			B2_ASSERT( chain->shapeIndices == NULL );
		}
	}

	int sensorCount = world->sensors.count;
	for ( int i = 0; i < sensorCount; ++i )
	{
		b2Array_Destroy( world->sensors.data[i].hits );
		b2Array_Destroy( world->sensors.data[i].overlaps1 );
		b2Array_Destroy( world->sensors.data[i].overlaps2 );
	}

	b2Array_Destroy( world->sensors );

	b2Array_Destroy( world->bodies );
	b2Array_Destroy( world->shapes );
	b2Array_Destroy( world->fatAABBs );
	b2Array_Destroy( world->chainShapes );
	b2Array_Destroy( world->contacts );
	b2Array_Destroy( world->joints );

	for ( int i = 0; i < world->islands.count; ++i )
	{
		b2Array_Destroy( world->islands.data[i].bodies );
		b2Array_Destroy( world->islands.data[i].contacts );
		b2Array_Destroy( world->islands.data[i].joints );
	}
	b2Array_Destroy( world->islands );

	// Destroy solver sets
	int setCapacity = world->solverSets.count;
	for ( int i = 0; i < setCapacity; ++i )
	{
		b2SolverSet* set = world->solverSets.data + i;
		if ( set->setIndex != B2_NULL_INDEX )
		{
			b2DestroySolverSet( world, i );
		}
	}

	b2Array_Destroy( world->solverSets );

	b2DestroyGraph( &world->constraintGraph );
	b2DestroyBroadPhase( &world->broadPhase );

	b2DestroyIdPool( &world->bodyIdPool );
	b2DestroyIdPool( &world->shapeIdPool );
	b2DestroyIdPool( &world->chainIdPool );
	b2DestroyIdPool( &world->contactIdPool );
	b2DestroyIdPool( &world->jointIdPool );
	b2DestroyIdPool( &world->islandIdPool );
	b2DestroyIdPool( &world->solverSetIdPool );

	b2DestroyStack( &world->stack );

	// Wipe world but preserve generation
	uint16_t generation = world->generation;
	*world = (b2World){ 0 };
	world->worldId = 0;
	world->generation = generation + 1;
}

static inline float b2RelativeCos( b2Rot a, b2Rot b )
{
	return a.c * b.c + a.s * b.s;
}

static inline b2BodySim* b2ResolveContactBodySim( b2World* world, b2BodySim* awakeSims, b2BodySim* staticSims, int encodedIndex,
												  int bodyId )
{
	if ( encodedIndex >= 0 )
	{
		return awakeSims + encodedIndex;
	}

	if ( b2IsStaticSimIndex( encodedIndex ) )
	{
		return staticSims - encodedIndex - 2;
	}

	return b2GetBodySim( world, b2Array_Get( world->bodies, bodyId ) );
}

static void b2CollideTask( int startIndex, int endIndex, int workerIndex, void* context )
{
	b2TracyCZoneNC( collide_task, "Collide", b2_colorDodgerBlue, true );

	b2StepContext* stepContext = context;
	b2World* world = stepContext->world;
	b2TaskContext* taskContext = world->taskContexts.data + workerIndex;
	const b2ContactCollideSpan* spans = stepContext->collideSpans;
	b2Shape* shapes = world->shapes.data;
	const b2AABB* fatAABBs = world->fatAABBs.data;
	b2SolverSet* solverSets = world->solverSets.data;
	b2BodySim* awakeSims = solverSets[b2_awakeSet].bodySims.data;
	b2BodySim* staticSims = solverSets[b2_staticSet].bodySims.data;

	B2_ASSERT( startIndex < endIndex );

	int spanIndex = 0;
	while ( spans[spanIndex + 1].start <= startIndex )
	{
		spanIndex += 1;
	}

	int spanStart = spans[spanIndex].start;
	int spanEnd = spans[spanIndex + 1].start;
	b2ContactSim* spanBase = spans[spanIndex].contacts;

	float recycleDistance = world->contactRecycleDistance;
	float speculativeDistance = B2_SPECULATIVE_DISTANCE;
	float recycleDistanceNonTouching = b2MinFloat( recycleDistance, speculativeDistance );

	for ( int contactIndex = startIndex; contactIndex < endIndex; ++contactIndex )
	{
		if ( contactIndex == spanEnd )
		{
			spanIndex += 1;
			spanStart = spans[spanIndex].start;
			spanEnd = spans[spanIndex + 1].start;
			spanBase = spans[spanIndex].contacts;
		}

		b2ContactSim* contactSim = spanBase + ( contactIndex - spanStart );

		int contactId = contactSim->contactId;
		int shapeIdA = contactSim->shapeIdA;
		int shapeIdB = contactSim->shapeIdB;

		// Do proxies still overlap?
		bool overlap = b2OverlapV( fatAABBs + shapeIdA, fatAABBs + shapeIdB );
		if ( overlap == false )
		{
			contactSim->simFlags |= b2_simDisjoint;
			contactSim->simFlags &= ~b2_simTouchingFlag;
			b2SetBit( &taskContext->contactStateBitSet, contactId );
		}
		else
		{
			b2Shape* shapeA = shapes + shapeIdA;
			b2Shape* shapeB = shapes + shapeIdB;

			bool wasTouching = ( contactSim->simFlags & b2_simTouchingFlag );

			// Update contact respecting shape/body order (A,B)
			int encodedA = contactSim->encodedBodySimA;
			int encodedB = contactSim->encodedBodySimB;
			b2BodySim* bodySimA = b2ResolveContactBodySim( world, awakeSims, staticSims, encodedA, shapeA->bodyId );
			b2BodySim* bodySimB = b2ResolveContactBodySim( world, awakeSims, staticSims, encodedB, shapeB->bodyId );
			b2WorldTransform transformA = bodySimA->transform;
			b2WorldTransform transformB = bodySimB->transform;

			// These may not be skipped by relative transform check below
			contactSim->invMassA = bodySimA->invMass;
			contactSim->invIA = bodySimA->invInertia;
			contactSim->invMassB = bodySimB->invMass;
			contactSim->invIB = bodySimB->invInertia;

			// Contact recycling optimization. Please cite this code if you use this optimization.
			// This is inspired by persistent contact manifolds used in some physics engines, such as PhysX.
			// However, this allows larger relative motion and has fewer tuning parameters (just one).
			if ( recycleDistance > 0.0f && ( contactSim->simFlags & b2_simRelativeTransformValid ) &&
				 ( contactSim->simFlags & b2_contactRecycleFlag ) )
			{
				b2Rot cachedQA = contactSim->cachedRotationA;
				b2Rot cachedQB = contactSim->cachedRotationB;
				b2Transform xfc = contactSim->cachedRelativePose;
				b2Transform xf = b2InvMulWorldTransforms( transformA, transformB );

				float cosA = b2RelativeCos( transformA.q, cachedQA );
				float cosB = b2RelativeCos( transformB.q, cachedQB );
				float minCos = b2MinFloat( cosA, cosB );

				float maxExtentA = b2IsStaticSimIndex( encodedA ) ? 0.0f : bodySimA->maxExtent;
				float maxExtentB = b2IsStaticSimIndex( encodedB ) ? 0.0f : bodySimB->maxExtent;
				float maxExtent = b2MaxFloat( maxExtentA, maxExtentB );
				float distance = b2Distance( xf.p, xfc.p );
				b2Rot qr = b2InvMulRot( xf.q, xfc.q );

				// This metric is used for fast bodies and sleeping. It comes from conservative advancement.
				// Note that qr.s == sin(theta) ~= theta for small angles.
				// Need a tighter tolerance for non-touching shapes so that contacts are not missed.
				float tolerance = wasTouching ? recycleDistance : recycleDistanceNonTouching;

				if ( minCos > B2_CONTACT_RECYCLE_COS_ANGLE && distance + maxExtent * b2AbsFloat( qr.s ) < tolerance )
				{
					b2Rot dqA = b2MulRot( transformA.q, b2InvertRot( cachedQA ) );
					b2Rot dqB = b2MulRot( transformB.q, b2InvertRot( cachedQB ) );
					b2Vec2 normal = contactSim->manifold.normal;

					// Minimize round-off
					b2Vec2 dc = b2SubPos( bodySimB->center, bodySimA->center );

					for ( int i = 0; i < contactSim->manifold.pointCount; ++i )
					{
						// Keep anchors but update separation, same as sub-stepping. This eliminates jitter.
						b2ManifoldPoint* mp = contactSim->manifold.points + i;
						b2Vec2 rA = b2RotateVector( dqA, mp->anchorA );
						b2Vec2 rB = b2RotateVector( dqB, mp->anchorB );
						b2Vec2 dp = b2Add( dc, b2Sub( rB, rA ) );
						
						mp->separation = mp->baseSeparation + b2Dot( dp, normal );
						mp->normalVelocity = 0.0f;
						mp->persisted = true;
					}

					taskContext->recycledContactCount += 1;

					// Contact is recycled. This also skips updating other aspects of the contact
					// such as material parameters.
					continue;
				}
			}

			// Caching for contact recycling.
			contactSim->cachedRotationA = transformA.q;
			contactSim->cachedRotationB = transformB.q;
			contactSim->cachedRelativePose = b2InvMulWorldTransforms( transformA, transformB );
			contactSim->simFlags |= b2_simRelativeTransformValid;

			b2Vec2 centerOffsetA = b2RotateVector( transformA.q, bodySimA->localCenter );
			b2Vec2 centerOffsetB = b2RotateVector( transformB.q, bodySimB->localCenter );

			// This updates solid contacts
			bool touching =
				b2UpdateContact( world, contactSim, shapeA, transformA, centerOffsetA, shapeB, transformB, centerOffsetB );

			// State changes that affect island connectivity. Also affects contact events.
			if ( touching == true && wasTouching == false )
			{
				contactSim->simFlags |= b2_simStartedTouching;
				b2SetBit( &taskContext->contactStateBitSet, contactId );
			}
			else if ( touching == false && wasTouching == true )
			{
				contactSim->simFlags |= b2_simStoppedTouching;
				b2SetBit( &taskContext->contactStateBitSet, contactId );
			}

			if ( touching )
			{
				for ( int i = 0; i < contactSim->manifold.pointCount; ++i )
				{
					// Cache separation
					b2ManifoldPoint* mp = contactSim->manifold.points + i;
					mp->baseSeparation = mp->separation;
				}
			}

			// To make this work, the time of impact code needs to adjust the target
			// distance based on the number of TOI events for a body.
			// if (touching && bodySimB->isFast)
			//{
			//	b2Manifold* manifold = &contactSim->manifold;
			//	int pointCount = manifold->pointCount;
			//	for (int i = 0; i < pointCount; ++i)
			//	{
			//		// trick the solver into pushing the fast shapes apart
			//		manifold->points[i].separation -= 0.25f * B2_SPECULATIVE_DISTANCE;
			//	}
			//}
		}
	}

	b2TracyCZoneEnd( collide_task );
}

static void b2AddNonTouchingContact( b2World* world, b2Contact* contact, b2ContactSim* contactSim )
{
	B2_ASSERT( contact->setIndex == b2_awakeSet );
	b2SolverSet* set = b2Array_Get( world->solverSets, b2_awakeSet );
	contact->colorIndex = B2_NULL_INDEX;
	contact->localIndex = set->contactSims.count;

	b2ContactSim* newContactSim = b2Array_Emplace( set->contactSims );
	memcpy( newContactSim, contactSim, sizeof( b2ContactSim ) );
}

static void b2RemoveNonTouchingContact( b2World* world, int setIndex, int localIndex )
{
	b2SolverSet* set = b2Array_Get( world->solverSets, setIndex );
	int movedIndex = b2Array_RemoveSwap( set->contactSims, localIndex );
	if ( movedIndex != B2_NULL_INDEX )
	{
		b2ContactSim* movedContactSim = set->contactSims.data + localIndex;
		b2Contact* movedContact = b2Array_Get( world->contacts, movedContactSim->contactId );
		B2_ASSERT( movedContact->setIndex == setIndex );
		B2_ASSERT( movedContact->localIndex == movedIndex );
		B2_ASSERT( movedContact->colorIndex == B2_NULL_INDEX );
		movedContact->localIndex = localIndex;
	}
}

// Narrow-phase collision
static void b2Collide( b2StepContext* context )
{
	b2World* world = context->world;

	B2_ASSERT( world->workerCount > 0 );

	b2TracyCZoneNC( collide, "Narrow Phase", b2_colorDodgerBlue, true );

	// gather contacts into a single array for easier parallel-for
	int contactCount = 0;
	b2GraphColor* graphColors = world->constraintGraph.colors;
	for ( int i = 0; i < B2_GRAPH_COLOR_COUNT; ++i )
	{
		contactCount += graphColors[i].contactSims.count;
	}

	int nonTouchingCount = world->solverSets.data[b2_awakeSet].contactSims.count;
	contactCount += nonTouchingCount;

	if ( contactCount == 0 )
	{
		b2TracyCZoneEnd( collide );
		return;
	}

	b2ContactCollideSpan collideSpans[B2_GRAPH_COLOR_COUNT + 2];
	int spanCount = 0;
	int contactIndex = 0;
	for ( int i = 0; i < B2_GRAPH_COLOR_COUNT; ++i )
	{
		b2GraphColor* color = graphColors + i;
		int count = color->contactSims.count;
		if ( count > 0 )
		{
			collideSpans[spanCount].start = contactIndex;
			collideSpans[spanCount].contacts = color->contactSims.data;
			spanCount += 1;
			contactIndex += count;
		}
	}

	if ( nonTouchingCount > 0 )
	{
		collideSpans[spanCount].start = contactIndex;
		collideSpans[spanCount].contacts = world->solverSets.data[b2_awakeSet].contactSims.data;
		spanCount += 1;
		contactIndex += nonTouchingCount;
	}

	B2_ASSERT( contactIndex == contactCount );
	B2_ASSERT( spanCount <= B2_GRAPH_COLOR_COUNT + 1 );

	collideSpans[spanCount].start = contactCount;
	collideSpans[spanCount].contacts = NULL;

	context->collideSpans = collideSpans;

	// Contact bit set on ids because contact pointers are unstable as they move between touching and not touching.
	int contactIdCapacity = b2GetIdCapacity( &world->contactIdPool );
	for ( int i = 0; i < world->workerCount; ++i )
	{
		b2SetBitCountAndClear( &world->taskContexts.data[i].contactStateBitSet, contactIdCapacity );
		world->taskContexts.data[i].recycledContactCount = 0;
	}

	// Task should take at least 40us on a 4GHz CPU (10K cycles)
	int minRange = 64;
	b2ParallelFor( world, &b2CollideTask, contactCount, minRange, context );

	context->collideSpans = NULL;

	// Serially update contact state
	b2TracyCZoneNC( contact_state, "Contact State", b2_colorLightSlateGray, true );

	// Bitwise OR all contact bits
	b2BitSet* bitSet = &world->taskContexts.data[0].contactStateBitSet;
	for ( int i = 1; i < world->workerCount; ++i )
	{
		b2InPlaceUnion( bitSet, &world->taskContexts.data[i].contactStateBitSet );
	}

	b2SolverSet* awakeSet = b2Array_Get( world->solverSets, b2_awakeSet );

	int endEventArrayIndex = world->endEventArrayIndex;

	const b2Shape* shapes = world->shapes.data;
	uint16_t worldId = world->worldId;

	// Process contact state changes. Iterate over set bits
	for ( uint32_t k = 0; k < bitSet->blockCount; ++k )
	{
		uint64_t bits = bitSet->bits[k];
		while ( bits != 0 )
		{
			uint32_t ctz = b2CTZ64( bits );
			int contactId = (int)( 64 * k + ctz );

			b2Contact* contact = b2Array_Get( world->contacts, contactId );
			B2_ASSERT( contact->setIndex == b2_awakeSet );

			int colorIndex = contact->colorIndex;
			int localIndex = contact->localIndex;

			b2ContactSim* contactSim = NULL;
			if ( colorIndex != B2_NULL_INDEX )
			{
				// contact lives in constraint graph
				B2_ASSERT( 0 <= colorIndex && colorIndex < B2_GRAPH_COLOR_COUNT );
				b2GraphColor* color = graphColors + colorIndex;
				contactSim = b2Array_Get( color->contactSims, localIndex );
			}
			else
			{
				contactSim = b2Array_Get( awakeSet->contactSims, localIndex );
			}

			uint32_t flags = contact->flags;
			uint32_t simFlags = contactSim->simFlags;

			if ( simFlags & b2_simDisjoint )
			{
				// Bounding boxes no longer overlap
				b2DestroyContact( world, contact );
				contact = NULL;
				contactSim = NULL;
			}
			else if ( simFlags & b2_simStartedTouching )
			{
				B2_ASSERT( contact->islandId == B2_NULL_INDEX );

				if ( flags & b2_contactEnableContactEvents )
				{
					const b2Shape* shapeA = shapes + contact->shapeIdA;
					const b2Shape* shapeB = shapes + contact->shapeIdB;
					b2ShapeId shapeIdA = { shapeA->id + 1, worldId, shapeA->generation };
					b2ShapeId shapeIdB = { shapeB->id + 1, worldId, shapeB->generation };
					b2ContactId contactFullId = {
						.index1 = contactId + 1,
						.world0 = worldId,
						.padding = 0,
						.generation = contact->generation,
					};

					b2ContactBeginTouchEvent event = { shapeIdA, shapeIdB, contactFullId };
					b2Array_Push( world->contactBeginEvents, event );
				}

				B2_ASSERT( contactSim->manifold.pointCount > 0 );
				B2_ASSERT( contact->setIndex == b2_awakeSet );

				// Link first because this wakes colliding bodies and ensures the body sims
				// are in the correct place.
				contact->flags |= b2_contactTouchingFlag;
				b2LinkContact( world, contact );

				// Make sure these didn't change
				B2_ASSERT( contact->colorIndex == B2_NULL_INDEX );
				B2_ASSERT( contact->localIndex == localIndex );

				// Contact sim pointer may have become orphaned due to awake set growth,
				// so I just need to refresh it.
				contactSim = b2Array_Get( awakeSet->contactSims, localIndex );

				contactSim->simFlags &= ~b2_simStartedTouching;

				// Add first for memcpy
				b2AddContactToGraph( world, contactSim, contact );

				// This destroys the contact sim
				b2RemoveNonTouchingContact( world, b2_awakeSet, localIndex );

				contactSim = NULL;
			}
			else if ( simFlags & b2_simStoppedTouching )
			{
				contactSim->simFlags &= ~b2_simStoppedTouching;
				contact->flags &= ~b2_contactTouchingFlag;

				if ( contact->flags & b2_contactEnableContactEvents )
				{
					const b2Shape* shapeA = shapes + contact->shapeIdA;
					const b2Shape* shapeB = shapes + contact->shapeIdB;
					b2ShapeId shapeIdA = { shapeA->id + 1, worldId, shapeA->generation };
					b2ShapeId shapeIdB = { shapeB->id + 1, worldId, shapeB->generation };
					b2ContactId contactFullId = {
						.index1 = contactId + 1,
						.world0 = worldId,
						.padding = 0,
						.generation = contact->generation,
					};

					b2ContactEndTouchEvent event = { shapeIdA, shapeIdB, contactFullId };
					b2Array_Push( world->contactEndEvents[endEventArrayIndex], event );
				}

				B2_ASSERT( contactSim->manifold.pointCount == 0 );

				b2UnlinkContact( world, contact );
				int bodyIdA = contact->edges[0].bodyId;
				int bodyIdB = contact->edges[1].bodyId;

				// Add first for memcpy
				b2AddNonTouchingContact( world, contact, contactSim );
				b2RemoveContactFromGraph( world, bodyIdA, bodyIdB, colorIndex, localIndex );
				contact = NULL;
				contactSim = NULL;
			}

			// Clear the smallest set bit
			bits = bits & ( bits - 1 );
		}
	}

	b2ValidateSolverSets( world );
	b2ValidateContacts( world );

	b2TracyCZoneEnd( contact_state );
	b2TracyCZoneEnd( collide );
}

void b2World_Step( b2WorldId worldId, float timeStep, int subStepCount )
{
	B2_CHECK_INPUT( b2IsValidFloat( timeStep ) );
	B2_ASSERT( 0 < subStepCount );

	b2World* world = b2GetWorldFromId( worldId );
	B2_ASSERT( world->locked == false );
	if ( world->locked )
	{
		b2TracyCFrame;
		return;
	}

	// Record step inputs before simulation runs
	B2_REC( world, Step, worldId, timeStep, subStepCount );

	// Prepare to capture events
	// Ensure user does not access stale data if there is an early return
	b2Array_Clear( world->bodyMoveEvents );
	b2Array_Clear( world->sensorBeginEvents );
	b2Array_Clear( world->contactBeginEvents );
	b2Array_Clear( world->contactHitEvents );
	b2Array_Clear( world->jointEvents );

	world->profile = (b2Profile){ 0 };

	b2TracyCZoneNC( world_step, "Step", b2_colorBox2DGreen, true );

	world->locked = true;
	world->activeTaskCount = 0;
	world->taskCount = 0;

	if ( world->scheduler != NULL )
	{
		b2ResetScheduler( world->scheduler );
	}

	uint64_t stepTicks = b2GetTicks();

	{
		b2Capacity* c = &world->maxCapacity;
		c->staticShapeCount = b2MaxInt( c->staticShapeCount, world->broadPhase.trees[b2_staticBody].proxyCount );
		c->dynamicShapeCount = b2MaxInt( c->dynamicShapeCount, world->broadPhase.trees[b2_dynamicBody].proxyCount );

		int staticBodyCount = world->solverSets.data[b2_staticSet].bodySims.count;
		c->staticBodyCount = b2MaxInt( c->staticBodyCount, staticBodyCount );

		// this includes kinematic bodies
		int totalBodyCount = b2GetIdCount( &world->bodyIdPool );
		c->dynamicBodyCount = b2MaxInt( c->dynamicBodyCount, totalBodyCount - staticBodyCount );

		int totalContactCount = b2GetIdCount( &world->contactIdPool );
		c->contactCount = b2MaxInt( c->contactCount, totalContactCount );
	}

	// Update collision pairs and create contacts
	{
		uint64_t pairTicks = b2GetTicks();
		b2UpdateBroadPhasePairs( world );
		world->profile.pairs = b2GetMilliseconds( pairTicks );
	}

	b2StepContext context = { 0 };
	context.world = world;
	context.dt = timeStep;
	context.subStepCount = b2MaxInt( 1, subStepCount );

	if ( timeStep > 0.0f )
	{
		context.inv_dt = 1.0f / timeStep;
		context.h = timeStep / context.subStepCount;
		context.inv_h = context.subStepCount * context.inv_dt;
	}
	else
	{
		context.inv_dt = 0.0f;
		context.h = 0.0f;
		context.inv_h = 0.0f;
	}

	world->inv_h = context.inv_h;
	world->inv_dt = context.inv_dt;

	// Hertz values get reduced for large time steps
	float contactHertz = b2MinFloat( world->contactHertz, 0.125f * context.inv_h );
	context.contactSoftness = b2MakeSoft( contactHertz, world->contactDampingRatio, context.h );
	context.staticSoftness = b2MakeSoft( 2.0f * contactHertz, world->contactDampingRatio, context.h );

	context.restitutionThreshold = world->restitutionThreshold;
	context.maxLinearVelocity = world->maxLinearSpeed;
	context.enableWarmStarting = world->enableWarmStarting;

	// Narrow phase : update contacts
	{
		uint64_t collideTicks = b2GetTicks();
		b2Collide( &context );
		world->profile.collide = b2GetMilliseconds( collideTicks );
	}

	// Integrate velocities, solve velocity constraints, and integrate positions.
	if ( timeStep > 0.0f )
	{
		uint64_t solveTicks = b2GetTicks();
		b2Solve( world, &context );
		world->profile.solve = b2GetMilliseconds( solveTicks );
	}

	// Finish the tree task in case b2Solve didn't finish it
	if ( world->userTreeTask )
	{
		world->finishTaskFcn( world->userTreeTask, world->userTaskContext );
		world->userTreeTask = NULL;
		world->activeTaskCount -= 1;
		b2ValidateNoEnlarged( &world->broadPhase );
	}

	// Update sensors
	{
		uint64_t sensorTicks = b2GetTicks();
		b2OverlapSensors( world );
		world->profile.sensors = b2GetMilliseconds( sensorTicks );
	}

	world->profile.step = b2GetMilliseconds( stepTicks );

	B2_ASSERT( b2GetStackAllocation( &world->stack ) == 0 );

	world->maxCapacity.arenaByteCount = b2MaxInt( world->maxCapacity.arenaByteCount, b2GetMaxStackAllocation( &world->stack ) );

	// Ensure stack is large enough
	b2GrowStack( &world->stack );

	// Make sure all tasks that were started were also finished
	B2_ASSERT( world->activeTaskCount == 0 );

	b2TracyCZoneEnd( world_step );

	// Swap end event array buffers
	world->endEventArrayIndex = 1 - world->endEventArrayIndex;
	b2Array_Clear( world->sensorEndEvents[world->endEventArrayIndex] );
	b2Array_Clear( world->contactEndEvents[world->endEventArrayIndex] );

	if ( world->recording != NULL )
	{
		// Write the per-step StateHash while the world is still locked. Queries early return while
		// locked, so this keeps the shared recording buffer single-writer without a lock. StateHash
		// proves the simulation reproduced exactly on replay.
		uint64_t hash = b2HashWorldState( world );
		b2RecArgs_StateHash stateHash = { worldId, hash };
		b2RecWrite_StateHash( world->recording, &stateHash );

		// Grow the recorded bounds so a replay can frame the whole motion, not just frame 0
		b2AABB bounds;
		if ( b2ComputeWorldBounds( world, &bounds ) )
		{
			b2RecAccumulateBounds( world->recording, bounds );
		}
	}

	world->locked = false;

	b2TracyCFrame;
}

static void b2DrawShape( b2DebugDraw* draw, b2Shape* shape, b2WorldTransform transform, b2HexColor color, bool drawChainNormals )
{
	switch ( shape->type )
	{
		case b2_capsuleShape:
		{
			b2Capsule* capsule = &shape->capsule;
			b2Pos p1 = b2TransformWorldPoint( transform, capsule->center1 );
			b2Pos p2 = b2TransformWorldPoint( transform, capsule->center2 );
			draw->DrawSolidCapsuleFcn( p1, p2, capsule->radius, color, draw->context );
		}
		break;

		case b2_circleShape:
		{
			b2Circle* circle = &shape->circle;
			draw->DrawSolidCircleFcn( transform, circle->center, circle->radius, color, draw->context );
		}
		break;

		case b2_polygonShape:
		{
			b2Polygon* poly = &shape->polygon;
			draw->DrawSolidPolygonFcn( transform, poly->vertices, poly->count, poly->radius, color, draw->context );
		}
		break;

		case b2_segmentShape:
		{
			b2Segment* segment = &shape->segment;
			b2Pos p1 = b2TransformWorldPoint( transform, segment->point1 );
			b2Pos p2 = b2TransformWorldPoint( transform, segment->point2 );
			draw->DrawLineFcn( p1, p2, color, draw->context );
		}
		break;

		case b2_chainSegmentShape:
		{
			b2Segment* segment = &shape->chainSegment.segment;
			b2Pos p1 = b2TransformWorldPoint( transform, segment->point1 );
			b2Pos p2 = b2TransformWorldPoint( transform, segment->point2 );
			draw->DrawLineFcn( p1, p2, color, draw->context );
			draw->DrawPointFcn( p2, 4.0f, color, draw->context );

			if ( drawChainNormals )
			{
				b2Pos c = b2LerpPos( p1, p2, 0.5f );
				b2Vec2 e = b2Normalize( b2SubPos( p2, p1 ) );
				b2Vec2 n = b2RightPerp( e );
				float L = 0.2f * b2GetLengthUnitsPerMeter();
				draw->DrawLineFcn( c, b2OffsetPos( c, b2MulSV( L, n ) ), b2_colorPaleGreen, draw->context );
			}
		}
		break;

		default:
			break;
	}
}

struct DrawContext
{
	b2World* world;
	b2DebugDraw* draw;
};

static bool DrawQueryCallback( int proxyId, uint64_t userData, void* context )
{
	B2_UNUSED( proxyId );

	int shapeId = (int)userData;

	struct DrawContext* drawContext = context;
	b2World* world = drawContext->world;
	b2DebugDraw* draw = drawContext->draw;

	b2Shape* shape = b2Array_Get( world->shapes, shapeId );
	B2_ASSERT( shape->id == shapeId );
	b2Body* body = b2Array_Get( world->bodies, shape->bodyId );
	b2BodySim* bodySim = b2GetBodySim( world, body );

	b2SetBit( &world->debugBodySet, shape->bodyId );

	if ( draw->drawShapes )
	{
		b2HexColor color;

		if ( shape->material.customColor != 0 )
		{
			color = shape->material.customColor;
		}
		else if ( body->type == b2_dynamicBody && body->mass == 0.0f )
		{
			// Bad body
			color = b2_colorRed;
		}
		else if ( body->setIndex == b2_disabledSet )
		{
			color = b2_colorSlateGray;
		}
		else if ( shape->sensorIndex != B2_NULL_INDEX )
		{
			color = b2_colorWheat;
		}
		else if ( bodySim->flags & b2_hadTimeOfImpact )
		{
			color = b2_colorLime;
		}
		else if ( ( bodySim->flags & b2_isBullet ) && body->setIndex == b2_awakeSet )
		{
			color = b2_colorTurquoise;
		}
		else if ( body->flags & b2_isSpeedCapped )
		{
			color = b2_colorYellow;
		}
		else if ( bodySim->flags & b2_isFast )
		{
			color = b2_colorSalmon;
		}
		else if ( body->type == b2_staticBody )
		{
			color = b2_colorPaleGreen;
		}
		else if ( body->type == b2_kinematicBody )
		{
			color = b2_colorRoyalBlue;
		}
		else if ( body->setIndex == b2_awakeSet )
		{
			color = b2_colorPink;
		}
		else
		{
			color = b2_colorGray;
		}

		b2DrawShape( draw, shape, bodySim->transform, color, draw->drawChainNormals );
	}

	if ( draw->drawBounds )
	{
		draw->DrawBoundsFcn( world->fatAABBs.data[shapeId], b2_colorGold, draw->context );
	}

	return true;
}

// todo this has varying order for moving shapes, causing flicker when overlapping shapes are moving
// solution: display order by shape id modulus 3, keep 3 buckets in GLSolid* and flush in 3 passes.
void b2World_Draw( b2WorldId worldId, b2DebugDraw* draw )
{
	b2World* world = b2GetWorldFromId( worldId );
	B2_ASSERT( world->locked == false );
	if ( world->locked )
	{
		return;
	}

	B2_ASSERT( b2IsValidAABB( draw->drawingBounds ) );

	const float k_axisScale = 0.3f;
	b2HexColor speculativeColor = b2_colorGainsboro;
	b2HexColor addColor = b2_colorGreen;
	b2HexColor persistColor = b2_colorBlue;
	b2HexColor normalColor = b2_colorDimGray;
	b2HexColor impulseColor = b2_colorMagenta;
	b2HexColor frictionColor = b2_colorYellow;

	int bodyCapacity = b2GetIdCapacity( &world->bodyIdPool );
	b2SetBitCountAndClear( &world->debugBodySet, bodyCapacity );

	int jointCapacity = b2GetIdCapacity( &world->jointIdPool );
	b2SetBitCountAndClear( &world->debugJointSet, jointCapacity );

	int contactCapacity = b2GetIdCapacity( &world->contactIdPool );
	b2SetBitCountAndClear( &world->debugContactSet, contactCapacity );

	int islandCapacity = b2GetIdCapacity( &world->islandIdPool );
	b2SetBitCountAndClear( &world->debugIslandSet, islandCapacity );

	struct DrawContext drawContext = { world, draw };

	for ( int i = 0; i < b2_bodyTypeCount; ++i )
	{
		b2DynamicTree_OverlapAABBAll( world->broadPhase.trees + i, draw->drawingBounds, DrawQueryCallback, &drawContext );
	}

	uint32_t wordCount = world->debugBodySet.blockCount;
	uint64_t* bits = world->debugBodySet.bits;
	for ( uint32_t k = 0; k < wordCount; ++k )
	{
		uint64_t word = bits[k];
		while ( word != 0 )
		{
			uint32_t ctz = b2CTZ64( word );
			uint32_t bodyId = 64 * k + ctz;

			b2Body* body = b2Array_Get( world->bodies, (int)bodyId );

			if ( draw->drawBodyNames && body->name[0] != 0 )
			{
				b2Vec2 offset = { 0.1f, 0.1f };
				b2BodySim* bodySim = b2GetBodySim( world, body );

				b2WorldTransform transform = { bodySim->center, bodySim->transform.q };
				b2Pos p = b2TransformWorldPoint( transform, offset );
				draw->DrawStringFcn( p, body->name, b2_colorWhiteSmoke, draw->context );
			}

			if ( draw->drawMass && body->type == b2_dynamicBody )
			{
				b2Vec2 offset = { 0.1f, 0.1f };
				b2BodySim* bodySim = b2GetBodySim( world, body );

				b2WorldTransform transform = { bodySim->center, bodySim->transform.q };
				draw->DrawLineFcn( bodySim->center0, bodySim->center, b2_colorWhiteSmoke, draw->context );
				draw->DrawTransformFcn( transform, draw->context );

				b2Pos p = b2TransformWorldPoint( transform, offset );
				char buffer[32];
				snprintf( buffer, 32, "  %.2f", body->mass );
				draw->DrawStringFcn( p, buffer, b2_colorWhite, draw->context );
			}

			if ( draw->drawJoints )
			{
				int jointKey = body->headJointKey;
				while ( jointKey != B2_NULL_INDEX )
				{
					int jointId = jointKey >> 1;
					int edgeIndex = jointKey & 1;
					b2Joint* joint = b2Array_Get( world->joints, jointId );

					// avoid double draw
					if ( b2GetBit( &world->debugJointSet, jointId ) == false )
					{
						b2DrawJoint( draw, world, joint );
						b2SetBit( &world->debugJointSet, jointId );
					}

					jointKey = joint->edges[edgeIndex].nextKey;
				}
			}

			const float linearSlop = B2_LINEAR_SLOP;
			if ( draw->drawContacts && body->type == b2_dynamicBody )
			{
				int contactKey = body->headContactKey;
				while ( contactKey != B2_NULL_INDEX )
				{
					int contactId = contactKey >> 1;
					int edgeIndex = contactKey & 1;
					b2Contact* contact = b2Array_Get( world->contacts, contactId );
					contactKey = contact->edges[edgeIndex].nextKey;

					// avoid double draw
					if ( b2GetBit( &world->debugContactSet, contactId ) == false )
					{
						b2ContactSim* contactSim = b2GetContactSim( world, contact );
						b2Body* bodyA = b2Array_Get( world->bodies, contact->edges[0].bodyId );
						b2BodySim* bodySimA = b2GetBodySim( world, bodyA );
						b2Body* bodyB = b2Array_Get( world->bodies, contact->edges[1].bodyId );
						b2BodySim* bodySimB = b2GetBodySim( world, bodyB );
						int pointCount = contactSim->manifold.pointCount;
						b2Vec2 normal = contactSim->manifold.normal;
						char buffer[32];

						for ( int j = 0; j < pointCount; ++j )
						{
							b2ManifoldPoint* mp = contactSim->manifold.points + j;

							b2Pos p;
							if ( draw->drawAnchorA )
							{
								p = b2OffsetPos( bodySimA->center, mp->anchorA );
							}
							else
							{
								p = b2OffsetPos( bodySimB->center, mp->anchorB );
							}

							if ( draw->drawGraphColors && contact->colorIndex != B2_NULL_INDEX )
							{
								// graph color
								float pointSize = contact->colorIndex == B2_OVERFLOW_INDEX ? 7.5f : 5.0f;
								draw->DrawPointFcn( p, pointSize, b2GetGraphColor( contact->colorIndex ), draw->context );
								// m_context->draw.DrawString(point->position, "%d", point->color);
							}
							else if ( mp->separation > linearSlop )
							{
								// Speculative
								draw->DrawPointFcn( p, 5.0f, speculativeColor, draw->context );
							}
							else if ( mp->persisted == false )
							{
								// Add
								draw->DrawPointFcn( p, 10.0f, addColor, draw->context );
							}
							else if ( mp->persisted == true )
							{
								// Persist
								draw->DrawPointFcn( p, 5.0f, persistColor, draw->context );
							}

							if ( draw->drawContactNormals )
							{
								b2Pos p1 = p;
								b2Pos p2 = b2OffsetPos( p1, b2MulSV( k_axisScale, normal ) );
								draw->DrawLineFcn( p1, p2, normalColor, draw->context );

								snprintf( buffer, B2_ARRAY_COUNT( buffer ), " %.2f", mp->separation );
								draw->DrawStringFcn( p1, buffer, b2_colorWhite, draw->context );
							}
							else if ( draw->drawContactForces )
							{
								// todo validate
								// multiply by one-half due to relax iteration
								float force = 0.5f * mp->totalNormalImpulse * world->inv_dt;
								b2Pos p1 = p;
								b2Pos p2 = b2OffsetPos( p1, b2MulSV( draw->forceScale * force, normal ) );
								draw->DrawLineFcn( p1, p2, impulseColor, draw->context );
								snprintf( buffer, B2_ARRAY_COUNT( buffer ), "%.1f", force );
								draw->DrawStringFcn( p1, buffer, b2_colorWhite, draw->context );
							}

							if ( draw->drawContactFeatures )
							{
								snprintf( buffer, B2_ARRAY_COUNT( buffer ), "%d", mp->id );
								draw->DrawStringFcn( p, buffer, b2_colorOrange, draw->context );
							}

							if ( draw->drawFrictionForces )
							{
								float force = 0.5f * mp->tangentImpulse * world->inv_h;
								b2Vec2 tangent = b2RightPerp( normal );
								b2Pos p1 = p;
								b2Pos p2 = b2OffsetPos( p1, b2MulSV( draw->forceScale * force, tangent ) );
								draw->DrawLineFcn( p1, p2, frictionColor, draw->context );
								snprintf( buffer, B2_ARRAY_COUNT( buffer ), "%.1f", force );
								draw->DrawStringFcn( p1, buffer, b2_colorWhite, draw->context );
							}
						}

						b2SetBit( &world->debugContactSet, contactId );
					}

					contactKey = contact->edges[edgeIndex].nextKey;
				}
			}

			if ( draw->drawIslands )
			{
				int islandId = body->islandId;
				if ( islandId != B2_NULL_INDEX && b2GetBit( &world->debugIslandSet, islandId ) == false )
				{
					b2Island* island = world->islands.data + islandId;
					if ( island->setIndex == B2_NULL_INDEX )
					{
						continue;
					}

					int shapeCount = 0;
					b2AABB aabb = {
						.lowerBound = { FLT_MAX, FLT_MAX },
						.upperBound = { -FLT_MAX, -FLT_MAX },
					};

					for ( int bodyIndex = 0; bodyIndex < island->bodies.count; ++bodyIndex )
					{
						int islandBodyId = island->bodies.data[bodyIndex];
						b2Body* islandBody = b2Array_Get( world->bodies, islandBodyId );
						int shapeId = islandBody->headShapeId;
						while ( shapeId != B2_NULL_INDEX )
						{
							b2Shape* shape = b2Array_Get( world->shapes, shapeId );
							aabb = b2AABB_Union( aabb, world->fatAABBs.data[shapeId] );
							shapeCount += 1;
							shapeId = shape->nextShapeId;
						}
					}

					if ( shapeCount > 0 )
					{
						draw->DrawBoundsFcn( aabb, b2_colorOrangeRed, draw->context );
					}

					b2SetBit( &world->debugIslandSet, islandId );
				}
			}

			// Clear the smallest set bit
			word = word & ( word - 1 );
		}
	}
}

bool b2ComputeWorldBounds( b2World* world, b2AABB* bounds )
{
	b2AABB worldBounds = { 0 };
	bool haveBounds = false;

	for ( int i = 0; i < b2_bodyTypeCount; ++i )
	{
		b2DynamicTree* tree = world->broadPhase.trees + i;
		if ( b2DynamicTree_GetProxyCount( tree ) == 0 )
		{
			continue;
		}

		b2AABB treeBounds = b2DynamicTree_GetRootBounds( tree );
		worldBounds = haveBounds ? b2AABB_Union( worldBounds, treeBounds ) : treeBounds;
		haveBounds = true;
	}

	*bounds = worldBounds;
	return haveBounds;
}

b2AABB b2World_GetBounds( b2WorldId worldId )
{
	b2World* world = b2GetUnlockedWorldFromId( worldId );
	if ( world == NULL )
	{
		return (b2AABB){ 0 };
	}

	b2AABB bounds;
	b2ComputeWorldBounds( world, &bounds );
	return bounds;
}

b2BodyEvents b2World_GetBodyEvents( b2WorldId worldId )
{
	b2World* world = b2GetWorldFromId( worldId );
	B2_ASSERT( world->locked == false );
	if ( world->locked )
	{
		return (b2BodyEvents){ 0 };
	}

	int count = world->bodyMoveEvents.count;
	b2BodyEvents events = { world->bodyMoveEvents.data, count };
	return events;
}

b2SensorEvents b2World_GetSensorEvents( b2WorldId worldId )
{
	b2World* world = b2GetWorldFromId( worldId );
	B2_ASSERT( world->locked == false );
	if ( world->locked )
	{
		return (b2SensorEvents){ 0 };
	}

	// Careful to use previous buffer
	int endEventArrayIndex = 1 - world->endEventArrayIndex;

	int beginCount = world->sensorBeginEvents.count;
	int endCount = world->sensorEndEvents[endEventArrayIndex].count;

	b2SensorEvents events = {
		.beginEvents = world->sensorBeginEvents.data,
		.endEvents = world->sensorEndEvents[endEventArrayIndex].data,
		.beginCount = beginCount,
		.endCount = endCount,
	};
	return events;
}

b2ContactEvents b2World_GetContactEvents( b2WorldId worldId )
{
	b2World* world = b2GetWorldFromId( worldId );
	B2_ASSERT( world->locked == false );
	if ( world->locked )
	{
		return (b2ContactEvents){ 0 };
	}

	// Careful to use previous buffer
	int endEventArrayIndex = 1 - world->endEventArrayIndex;

	int beginCount = world->contactBeginEvents.count;
	int endCount = world->contactEndEvents[endEventArrayIndex].count;
	int hitCount = world->contactHitEvents.count;

	b2ContactEvents events = {
		.beginEvents = world->contactBeginEvents.data,
		.endEvents = world->contactEndEvents[endEventArrayIndex].data,
		.hitEvents = world->contactHitEvents.data,
		.beginCount = beginCount,
		.endCount = endCount,
		.hitCount = hitCount,
	};

	return events;
}

b2JointEvents b2World_GetJointEvents( b2WorldId worldId )
{
	b2World* world = b2GetWorldFromId( worldId );
	B2_ASSERT( world->locked == false );
	if ( world->locked )
	{
		return (b2JointEvents){ 0 };
	}

	int count = world->jointEvents.count;
	b2JointEvents events = { world->jointEvents.data, count };
	return events;
}

bool b2World_IsValid( b2WorldId id )
{
	if ( id.index1 < 1 || B2_MAX_WORLDS < id.index1 )
	{
		return false;
	}

	b2World* world = b2_worlds + ( id.index1 - 1 );

	if ( world->worldId != id.index1 - 1 )
	{
		// world is not allocated
		return false;
	}

	return id.generation == world->generation;
}

bool b2Body_IsValid( b2BodyId id )
{
	if ( B2_MAX_WORLDS <= id.world0 )
	{
		// invalid world
		return false;
	}

	b2World* world = b2_worlds + id.world0;
	if ( world->worldId != id.world0 )
	{
		// world is free
		return false;
	}

	if ( id.index1 < 1 || world->bodies.count < id.index1 )
	{
		// invalid index
		return false;
	}

	b2Body* body = world->bodies.data + ( id.index1 - 1 );
	if ( body->setIndex == B2_NULL_INDEX )
	{
		// this was freed
		return false;
	}

	B2_ASSERT( body->localIndex != B2_NULL_INDEX );

	if ( body->generation != id.generation )
	{
		// this id is orphaned
		return false;
	}

	return true;
}

bool b2Shape_IsValid( b2ShapeId id )
{
	if ( B2_MAX_WORLDS <= id.world0 )
	{
		return false;
	}

	b2World* world = b2_worlds + id.world0;
	if ( world->worldId != id.world0 )
	{
		// world is free
		return false;
	}

	int shapeId = id.index1 - 1;
	if ( shapeId < 0 || world->shapes.count <= shapeId )
	{
		return false;
	}

	b2Shape* shape = world->shapes.data + shapeId;
	if ( shape->id == B2_NULL_INDEX )
	{
		// shape is free
		return false;
	}

	B2_ASSERT( shape->id == shapeId );

	return id.generation == shape->generation;
}

bool b2Chain_IsValid( b2ChainId id )
{
	if ( B2_MAX_WORLDS <= id.world0 )
	{
		return false;
	}

	b2World* world = b2_worlds + id.world0;
	if ( world->worldId != id.world0 )
	{
		// world is free
		return false;
	}

	int chainId = id.index1 - 1;
	if ( chainId < 0 || world->chainShapes.count <= chainId )
	{
		return false;
	}

	b2ChainShape* chain = world->chainShapes.data + chainId;
	if ( chain->id == B2_NULL_INDEX )
	{
		// chain is free
		return false;
	}

	B2_ASSERT( chain->id == chainId );

	return id.generation == chain->generation;
}

bool b2Joint_IsValid( b2JointId id )
{
	if ( B2_MAX_WORLDS <= id.world0 )
	{
		return false;
	}

	b2World* world = b2_worlds + id.world0;
	if ( world->worldId != id.world0 )
	{
		// world is free
		return false;
	}

	int jointId = id.index1 - 1;
	if ( jointId < 0 || world->joints.count <= jointId )
	{
		return false;
	}

	b2Joint* joint = world->joints.data + jointId;
	if ( joint->jointId == B2_NULL_INDEX )
	{
		// joint is free
		return false;
	}

	B2_ASSERT( joint->jointId == jointId );

	return id.generation == joint->generation;
}

bool b2Contact_IsValid( b2ContactId id )
{
	if ( B2_MAX_WORLDS <= id.world0 )
	{
		return false;
	}

	b2World* world = b2_worlds + id.world0;
	if ( world->worldId != id.world0 )
	{
		// world is free
		return false;
	}

	int contactId = id.index1 - 1;
	if ( contactId < 0 || world->contacts.count <= contactId )
	{
		return false;
	}

	b2Contact* contact = world->contacts.data + contactId;
	if ( contact->contactId == B2_NULL_INDEX )
	{
		// contact is free
		return false;
	}

	B2_ASSERT( contact->contactId == contactId );

	return id.generation == contact->generation;
}

void b2World_EnableSleeping( b2WorldId worldId, bool flag )
{
	b2World* world = b2GetWorldFromId( worldId );
	B2_ASSERT( world->locked == false );
	if ( world->locked )
	{
		return;
	}

	B2_REC( world, WorldEnableSleeping, worldId, flag );

	if ( flag == world->enableSleep )
	{
		return;
	}

	world->enableSleep = flag;

	if ( flag == false )
	{
		int setCount = world->solverSets.count;
		for ( int i = b2_firstSleepingSet; i < setCount; ++i )
		{
			b2SolverSet* set = b2Array_Get( world->solverSets, i );
			if ( set->bodySims.count > 0 )
			{
				b2WakeSolverSet( world, i );
			}
		}
	}
}

bool b2World_IsSleepingEnabled( b2WorldId worldId )
{
	b2World* world = b2GetWorldFromId( worldId );
	return world->enableSleep;
}

void b2World_EnableWarmStarting( b2WorldId worldId, bool flag )
{
	b2World* world = b2GetWorldFromId( worldId );
	B2_ASSERT( world->locked == false );
	if ( world->locked )
	{
		return;
	}

	B2_REC( world, WorldEnableWarmStarting, worldId, flag );

	world->enableWarmStarting = flag;
}

bool b2World_IsWarmStartingEnabled( b2WorldId worldId )
{
	b2World* world = b2GetWorldFromId( worldId );
	return world->enableWarmStarting;
}

void b2World_EnableSSE2Fallback( b2WorldId worldId, bool flag )
{
	b2World* world = b2GetWorldFromId( worldId );
	B2_ASSERT( world->locked == false );
	if ( world->locked )
	{
		return;
	}

	world->simdWidth = flag ? 4 : b2GetSIMDWidth();
}

int b2World_GetAwakeBodyCount( b2WorldId worldId )
{
	b2World* world = b2GetWorldFromId( worldId );
	b2SolverSet* awakeSet = b2Array_Get( world->solverSets, b2_awakeSet );
	return awakeSet->bodySims.count;
}

void b2World_EnableContinuous( b2WorldId worldId, bool flag )
{
	b2World* world = b2GetWorldFromId( worldId );
	B2_ASSERT( world->locked == false );
	if ( world->locked )
	{
		return;
	}

	B2_REC( world, WorldEnableContinuous, worldId, flag );

	world->enableContinuous = flag;
}

bool b2World_IsContinuousEnabled( b2WorldId worldId )
{
	b2World* world = b2GetWorldFromId( worldId );
	return world->enableContinuous;
}

void b2World_SetRestitutionThreshold( b2WorldId worldId, float value )
{
	B2_CHECK_INPUT( b2IsValidFloat( value ) );

	b2World* world = b2GetWorldFromId( worldId );
	B2_ASSERT( world->locked == false );
	if ( world->locked )
	{
		return;
	}

	B2_REC( world, WorldSetRestitutionThreshold, worldId, value );

	world->restitutionThreshold = b2ClampFloat( value, 0.0f, FLT_MAX );
}

float b2World_GetRestitutionThreshold( b2WorldId worldId )
{
	b2World* world = b2GetWorldFromId( worldId );
	return world->restitutionThreshold;
}

void b2World_SetRestitutionIterations( b2WorldId worldId, int iterations )
{
	B2_CHECK_INPUT( iterations >= 0 );

	b2World* world = b2GetWorldFromId( worldId );
	B2_ASSERT( world->locked == false );
	if ( world->locked )
	{
		return;
	}

	B2_REC( world, WorldSetRestitutionIterations, worldId, iterations );

	world->restitutionIterations = b2ClampInt( iterations, 0, B2_MAX_RESTITUTION_ITERATIONS );
}

int b2World_GetRestitutionIterations( b2WorldId worldId )
{
	b2World* world = b2GetWorldFromId( worldId );
	return world->restitutionIterations;
}

void b2World_EnableRestitutionPropagation( b2WorldId worldId, bool flag )
{
	b2World* world = b2GetWorldFromId( worldId );
	B2_ASSERT( world->locked == false );
	if ( world->locked )
	{
		return;
	}

	B2_REC( world, WorldEnableRestitutionPropagation, worldId, flag );

	world->enableRestitutionPropagation = flag;
}

bool b2World_IsRestitutionPropagationEnabled( b2WorldId worldId )
{
	b2World* world = b2GetWorldFromId( worldId );
	return world->enableRestitutionPropagation;
}

void b2World_SetHitEventThreshold( b2WorldId worldId, float value )
{
	B2_CHECK_INPUT( b2IsValidFloat( value ) );

	b2World* world = b2GetWorldFromId( worldId );
	B2_ASSERT( world->locked == false );
	if ( world->locked )
	{
		return;
	}

	B2_REC( world, WorldSetHitEventThreshold, worldId, value );

	world->hitEventThreshold = b2ClampFloat( value, 0.0f, FLT_MAX );
}

float b2World_GetHitEventThreshold( b2WorldId worldId )
{
	b2World* world = b2GetWorldFromId( worldId );
	return world->hitEventThreshold;
}

void b2World_SetContactTuning( b2WorldId worldId, float hertz, float dampingRatio, float pushSpeed )
{
	B2_CHECK_INPUT( b2IsValidFloat( hertz ) );
	B2_CHECK_INPUT( b2IsValidFloat( dampingRatio ) );
	B2_CHECK_INPUT( b2IsValidFloat( pushSpeed ) );

	b2World* world = b2GetWorldFromId( worldId );
	B2_ASSERT( world->locked == false );
	if ( world->locked )
	{
		return;
	}

	B2_REC( world, WorldSetContactTuning, worldId, hertz, dampingRatio, pushSpeed );

	world->contactHertz = b2ClampFloat( hertz, 0.0f, FLT_MAX );
	world->contactDampingRatio = b2ClampFloat( dampingRatio, 0.0f, FLT_MAX );
	world->contactSpeed = b2ClampFloat( pushSpeed, 0.0f, FLT_MAX );
}

void b2World_GetContactTuning( b2WorldId worldId, float* hertz, float* dampingRatio, float* pushSpeed )
{
	b2World* world = b2GetWorldFromId( worldId );
	*hertz = world->contactHertz;
	*dampingRatio = world->contactDampingRatio;
	*pushSpeed = world->contactSpeed;
}

void b2World_SetContactRecycleDistance( b2WorldId worldId, float recycleDistance )
{
	B2_CHECK_INPUT( b2IsValidFloat( recycleDistance ) );

	b2World* world = b2GetWorldFromId( worldId );
	B2_ASSERT( world->locked == false );
	if ( world->locked )
	{
		return;
	}

	B2_REC( world, WorldSetContactRecycleDistance, worldId, recycleDistance );

	world->contactRecycleDistance = b2ClampFloat( recycleDistance, 0.0f, FLT_MAX );
}

float b2World_GetContactRecycleDistance( b2WorldId worldId )
{
	b2World* world = b2GetWorldFromId( worldId );
	return world->contactRecycleDistance;
}

void b2World_SetMaxLinearSpeed( b2WorldId worldId, float maximumLinearSpeed )
{
	B2_CHECK_INPUT( b2IsValidFloat( maximumLinearSpeed ) && maximumLinearSpeed > 0.0f );

	b2World* world = b2GetWorldFromId( worldId );
	B2_ASSERT( world->locked == false );
	if ( world->locked )
	{
		return;
	}

	B2_REC( world, WorldSetMaxLinearSpeed, worldId, maximumLinearSpeed );

	world->maxLinearSpeed = maximumLinearSpeed;
}

float b2World_GetMaxLinearSpeed( b2WorldId worldId )
{
	b2World* world = b2GetWorldFromId( worldId );
	return world->maxLinearSpeed;
}

b2Profile b2World_GetProfile( b2WorldId worldId )
{
	b2World* world = b2GetWorldFromId( worldId );
	return world->profile;
}

b2Counters b2World_GetCounters( b2WorldId worldId )
{
	b2World* world = b2GetWorldFromId( worldId );
	b2Counters s = { 0 };
	s.bodyCount = b2GetIdCount( &world->bodyIdPool );
	s.shapeCount = b2GetIdCount( &world->shapeIdPool );
	s.contactCount = b2GetIdCount( &world->contactIdPool );
	s.jointCount = b2GetIdCount( &world->jointIdPool );
	s.islandCount = b2GetIdCount( &world->islandIdPool );

	b2DynamicTree* staticTree = world->broadPhase.trees + b2_staticBody;
	s.staticTreeHeight = b2DynamicTree_GetHeight( staticTree );

	b2DynamicTree* dynamicTree = world->broadPhase.trees + b2_dynamicBody;
	b2DynamicTree* kinematicTree = world->broadPhase.trees + b2_kinematicBody;
	s.treeHeight = b2MaxInt( b2DynamicTree_GetHeight( dynamicTree ), b2DynamicTree_GetHeight( kinematicTree ) );

	s.stackUsed = b2GetMaxStackAllocation( &world->stack );
	s.byteCount = b2GetByteCount();
	s.taskCount = world->taskCount;

	s.recycledContactCount = 0;
	for ( int i = 0; i < world->workerCount; ++i )
	{
		s.recycledContactCount += world->taskContexts.data[i].recycledContactCount;
	}

	s.awakeContactCount = 0;
	for ( int i = 0; i < B2_GRAPH_COLOR_COUNT; ++i )
	{
		b2GraphColor* color = world->constraintGraph.colors + i;
		s.colorCounts[i] = color->contactSims.count + color->jointSims.count;
		s.awakeContactCount += color->contactSims.count;
	}
	s.awakeContactCount += world->solverSets.data[b2_awakeSet].contactSims.count;

	return s;
}

b2Capacity b2World_GetMaxCapacity( b2WorldId worldId )
{
	b2World* world = b2GetWorldFromId( worldId );
	return world->maxCapacity;
}

void b2World_SetUserData( b2WorldId worldId, void* userData )
{
	b2World* world = b2GetWorldFromId( worldId );
	world->userData = userData;
}

void* b2World_GetUserData( b2WorldId worldId )
{
	b2World* world = b2GetWorldFromId( worldId );
	return world->userData;
}

void b2World_SetFrictionCallback( b2WorldId worldId, b2FrictionCallbackFcn* callback )
{
	b2World* world = b2GetWorldFromId( worldId );
	if ( world->locked )
	{
		return;
	}

	world->frictionCallback = callback;
}

void b2World_SetRestitutionCallback( b2WorldId worldId, b2RestitutionCallbackFcn* callback )
{
	b2World* world = b2GetWorldFromId( worldId );
	if ( world->locked )
	{
		return;
	}

	world->restitutionCallback = callback;
}

void b2World_SetWorkerCount( b2WorldId worldId, int count )
{
	b2World* world = b2GetUnlockedWorldFromId( worldId );
	if ( world == NULL )
	{
		return;
	}

	if ( count == world->workerCount )
	{
		return;
	}

	b2DestroyWorkerContexts( world );
	world->workerCount = b2ClampInt( count, 1, B2_MAX_WORKERS );
	b2CreateWorkerContexts( world );
}

int b2World_GetWorkerCount( b2WorldId worldId )
{
	b2World* world = b2GetUnlockedWorldFromId( worldId );
	if ( world == NULL )
	{
		return 0;
	}

	return world->workerCount;
}

void b2World_StartRecording( b2WorldId worldId, b2Recording* recording )
{
	// Must be a step boundary, so refuse a locked world
	b2World* world = b2GetUnlockedWorldFromId( worldId );

	if ( world == NULL || recording == NULL || world->recording != NULL )
	{
		return;
	}

	if ( world->preSolveFcn != NULL || world->preContinuousFcn != NULL || world->customFilterFcn != NULL )
	{
		printf( "b2World_StartRecording: preSolve and custom filtering are not supported when recording\n" );
		return;
	}

	b2StartRecordingIntoBuffer( world, recording );
}

void b2World_StopRecording( b2WorldId worldId )
{
	b2World* world = b2GetUnlockedWorldFromId( worldId );
	if ( world == NULL )
	{
		return;
	}

	b2StopRecordingInternal( world );
}

void b2World_DumpMemoryStats( b2WorldId worldId )
{
	FILE* file = fopen( "box2d_memory.txt", "w" );
	if ( file == NULL )
	{
		return;
	}

	b2World* world = b2GetWorldFromId( worldId );

	int total = 0;

	// id pools
	int bodyIdBytes = b2GetIdBytes( &world->bodyIdPool );
	int solverSetIdBytes = b2GetIdBytes( &world->solverSetIdPool );
	int jointIdBytes = b2GetIdBytes( &world->jointIdPool );
	int contactIdBytes = b2GetIdBytes( &world->contactIdPool );
	int islandIdBytes = b2GetIdBytes( &world->islandIdPool );
	int shapeIdBytes = b2GetIdBytes( &world->shapeIdPool );
	int chainIdBytes = b2GetIdBytes( &world->chainIdPool );
	total += bodyIdBytes + solverSetIdBytes + jointIdBytes + contactIdBytes + islandIdBytes + shapeIdBytes + chainIdBytes;

	fprintf( file, "id pools\n" );
	fprintf( file, "body ids: %d\n", bodyIdBytes );
	fprintf( file, "solver set ids: %d\n", solverSetIdBytes );
	fprintf( file, "joint ids: %d\n", jointIdBytes );
	fprintf( file, "contact ids: %d\n", contactIdBytes );
	fprintf( file, "island ids: %d\n", islandIdBytes );
	fprintf( file, "shape ids: %d\n", shapeIdBytes );
	fprintf( file, "chain ids: %d\n", chainIdBytes );
	fprintf( file, "\n" );

	// Islands own per-island body/contact/joint link arrays
	int islandLinkBytes = 0;
	for ( int i = 0; i < world->islands.count; ++i )
	{
		b2Island* island = world->islands.data + i;
		islandLinkBytes += b2Array_ByteCount( island->bodies );
		islandLinkBytes += b2Array_ByteCount( island->contacts );
		islandLinkBytes += b2Array_ByteCount( island->joints );
	}

	// world arrays
	int bodyArrayBytes = b2Array_ByteCount( world->bodies );
	int solverSetArrayBytes = b2Array_ByteCount( world->solverSets );
	int jointArrayBytes = b2Array_ByteCount( world->joints );
	int contactArrayBytes = b2Array_ByteCount( world->contacts );
	int islandArrayBytes = b2Array_ByteCount( world->islands );
	int shapeArrayBytes = b2Array_ByteCount( world->shapes ) + b2Array_ByteCount( world->fatAABBs );
	int chainArrayBytes = b2Array_ByteCount( world->chainShapes );
	int sensorArrayBytes = b2Array_ByteCount( world->sensors );
	total += bodyArrayBytes + solverSetArrayBytes + jointArrayBytes + contactArrayBytes + islandArrayBytes + islandLinkBytes +
			 shapeArrayBytes + chainArrayBytes + sensorArrayBytes;

	fprintf( file, "world arrays\n" );
	fprintf( file, "bodies: %d\n", bodyArrayBytes );
	fprintf( file, "solver sets: %d\n", solverSetArrayBytes );
	fprintf( file, "joints: %d\n", jointArrayBytes );
	fprintf( file, "contacts: %d\n", contactArrayBytes );
	fprintf( file, "islands: %d\n", islandArrayBytes );
	fprintf( file, "island links: %d\n", islandLinkBytes );
	fprintf( file, "shapes: %d\n", shapeArrayBytes );
	fprintf( file, "chains: %d\n", chainArrayBytes );
	fprintf( file, "sensors: %d\n", sensorArrayBytes );
	fprintf( file, "\n" );

	// Chain shapes own index and surface material arrays
	int chainDataBytes = 0;
	for ( int i = 0; i < world->chainShapes.count; ++i )
	{
		b2ChainShape* chain = world->chainShapes.data + i;
		if ( chain->id == B2_NULL_INDEX )
		{
			continue;
		}

		chainDataBytes += chain->segmentCount * (int)sizeof( int );
	}

	// Sensors own overlap tracking arrays. The sensor array is dense.
	int sensorOverlapBytes = 0;
	for ( int i = 0; i < world->sensors.count; ++i )
	{
		b2Sensor* sensor = world->sensors.data + i;
		sensorOverlapBytes += b2Array_ByteCount( sensor->hits );
		sensorOverlapBytes += b2Array_ByteCount( sensor->overlaps1 );
		sensorOverlapBytes += b2Array_ByteCount( sensor->overlaps2 );
	}
	total += chainDataBytes + sensorOverlapBytes;

	fprintf( file, "owned arrays\n" );
	fprintf( file, "chain data: %d\n", chainDataBytes );
	fprintf( file, "sensor overlaps: %d\n", sensorOverlapBytes );
	fprintf( file, "\n" );

	// broad-phase
	int staticTreeBytes = (int)b2DynamicTree_GetByteCount( world->broadPhase.trees + b2_staticBody );
	int kinematicTreeBytes = (int)b2DynamicTree_GetByteCount( world->broadPhase.trees + b2_kinematicBody );
	int dynamicTreeBytes = (int)b2DynamicTree_GetByteCount( world->broadPhase.trees + b2_dynamicBody );
	b2HashSet* pairSet = &world->broadPhase.pairSet;
	int pairSetBytes = b2GetHashSetBytes( pairSet );
	total += staticTreeBytes + kinematicTreeBytes + dynamicTreeBytes + pairSetBytes;

	fprintf( file, "broad-phase\n" );
	fprintf( file, "static tree: %d\n", staticTreeBytes );
	fprintf( file, "kinematic tree: %d\n", kinematicTreeBytes );
	fprintf( file, "dynamic tree: %d\n", dynamicTreeBytes );
	fprintf( file, "pairSet: %d (%u, %u)\n", pairSetBytes, pairSet->count, pairSet->capacity );
	fprintf( file, "\n" );

	// solver sets
	int bodySimCapacity = 0;
	int bodyStateCapacity = 0;
	int jointSimCapacity = 0;
	int contactSimCapacity = 0;
	int islandSimCapacity = 0;
	int solverSetCapacity = world->solverSets.count;
	for ( int i = 0; i < solverSetCapacity; ++i )
	{
		b2SolverSet* set = world->solverSets.data + i;
		if ( set->setIndex == B2_NULL_INDEX )
		{
			continue;
		}

		bodySimCapacity += set->bodySims.capacity;
		bodyStateCapacity += set->bodyStates.capacity;
		jointSimCapacity += set->jointSims.capacity;
		contactSimCapacity += set->contactSims.capacity;
		islandSimCapacity += set->islandSims.capacity;
	}

	int setBodySimBytes = bodySimCapacity * (int)sizeof( b2BodySim );
	int setBodyStateBytes = bodyStateCapacity * (int)sizeof( b2BodyState );
	int setJointSimBytes = jointSimCapacity * (int)sizeof( b2JointSim );
	int setContactSimBytes = contactSimCapacity * (int)sizeof( b2ContactSim );
	int setIslandSimBytes = islandSimCapacity * (int)sizeof( b2IslandSim );
	total += setBodySimBytes + setBodyStateBytes + setJointSimBytes + setContactSimBytes + setIslandSimBytes;

	fprintf( file, "solver sets\n" );
	fprintf( file, "body sim: %d\n", setBodySimBytes );
	fprintf( file, "body state: %d\n", setBodyStateBytes );
	fprintf( file, "joint sim: %d\n", setJointSimBytes );
	fprintf( file, "contact sim: %d\n", setContactSimBytes );
	fprintf( file, "island sim: %d\n", setIslandSimBytes );
	fprintf( file, "\n" );

	// constraint graph
	int bodyBitSetBytes = 0;
	contactSimCapacity = 0;
	jointSimCapacity = 0;
	for ( int i = 0; i < B2_GRAPH_COLOR_COUNT; ++i )
	{
		b2GraphColor* c = world->constraintGraph.colors + i;
		bodyBitSetBytes += b2GetBitSetBytes( &c->bodySet );
		contactSimCapacity += c->contactSims.capacity;
		jointSimCapacity += c->jointSims.capacity;
	}

	int graphJointSimBytes = jointSimCapacity * (int)sizeof( b2JointSim );
	int graphContactSimBytes = contactSimCapacity * (int)sizeof( b2ContactSim );
	total += bodyBitSetBytes + graphJointSimBytes + graphContactSimBytes;

	fprintf( file, "constraint graph\n" );
	fprintf( file, "body bit sets: %d\n", bodyBitSetBytes );
	fprintf( file, "joint sim: %d\n", graphJointSimBytes );
	fprintf( file, "contact sim: %d\n", graphContactSimBytes );
	fprintf( file, "\n" );

	// Per worker task storage and its bit sets
	int taskContextBytes = b2Array_ByteCount( world->taskContexts );
	for ( int i = 0; i < world->taskContexts.count; ++i )
	{
		b2TaskContext* taskContext = world->taskContexts.data + i;
		taskContextBytes += b2Array_ByteCount( taskContext->sensorHits );
		taskContextBytes += b2Array_ByteCount( taskContext->pairKeys );
		taskContextBytes += b2GetBitSetBytes( &taskContext->contactStateBitSet );
		taskContextBytes += b2GetBitSetBytes( &taskContext->hitEventBitSet );
		taskContextBytes += b2GetBitSetBytes( &taskContext->jointStateBitSet );
		taskContextBytes += b2GetBitSetBytes( &taskContext->enlargedSimBitSet );
		taskContextBytes += b2GetBitSetBytes( &taskContext->awakeIslandBitSet );
	}

	int sensorTaskContextBytes = b2Array_ByteCount( world->sensorTaskContexts );
	for ( int i = 0; i < world->sensorTaskContexts.count; ++i )
	{
		b2SensorTaskContext* taskContext = world->sensorTaskContexts.data + i;
		sensorTaskContextBytes += b2GetBitSetBytes( &taskContext->eventBits );
	}
	total += taskContextBytes + sensorTaskContextBytes;

	fprintf( file, "task contexts\n" );
	fprintf( file, "worker: %d\n", taskContextBytes );
	fprintf( file, "sensor: %d\n", sensorTaskContextBytes );
	fprintf( file, "\n" );

	// Double buffered event arrays
	int eventBytes = 0;
	eventBytes += b2Array_ByteCount( world->bodyMoveEvents );
	eventBytes += b2Array_ByteCount( world->sensorBeginEvents );
	eventBytes += b2Array_ByteCount( world->contactBeginEvents );
	eventBytes += b2Array_ByteCount( world->sensorEndEvents[0] );
	eventBytes += b2Array_ByteCount( world->sensorEndEvents[1] );
	eventBytes += b2Array_ByteCount( world->contactEndEvents[0] );
	eventBytes += b2Array_ByteCount( world->contactEndEvents[1] );
	eventBytes += b2Array_ByteCount( world->contactHitEvents );
	eventBytes += b2Array_ByteCount( world->jointEvents );
	total += eventBytes;

	fprintf( file, "events: %d\n\n", eventBytes );

	// Debug draw bit sets
	int debugBytes = 0;
	debugBytes += b2GetBitSetBytes( &world->debugBodySet );
	debugBytes += b2GetBitSetBytes( &world->debugJointSet );
	debugBytes += b2GetBitSetBytes( &world->debugContactSet );
	debugBytes += b2GetBitSetBytes( &world->debugIslandSet );
	total += debugBytes;

	fprintf( file, "debug draw: %d\n\n", debugBytes );

	// stack allocator
	total += world->stack.capacity;
	fprintf( file, "stack allocator: %d\n\n", world->stack.capacity );

	fprintf( file, "total: %d\n", total );

	fclose( file );
}

typedef struct WorldQueryContext
{
	b2World* world;
	b2OverlapResultCallbackFcn* fcn;
	b2QueryFilter filter;
	void* userContext;
} WorldQueryContext;

static bool TreeQueryCallback( int proxyId, uint64_t userData, void* context )
{
	B2_UNUSED( proxyId );

	int shapeId = (int)userData;

	WorldQueryContext* worldContext = context;
	b2World* world = worldContext->world;

	b2Shape* shape = b2Array_Get( world->shapes, shapeId );

	if ( b2ShouldQueryCollide( shape->filter, worldContext->filter ) == false )
	{
		return true;
	}

	b2ShapeId id = { shapeId + 1, world->worldId, shape->generation };
	bool result = worldContext->fcn( id, worldContext->userContext );
	return result;
}

b2TreeStats b2World_OverlapAABB( b2WorldId worldId, b2Pos origin, b2AABB aabb, b2QueryFilter filter, b2OverlapResultCallbackFcn* fcn,
								 void* context )
{
	b2TreeStats treeStats = { 0 };

	b2World* world = b2GetWorldFromId( worldId );
	B2_ASSERT( world->locked == false );
	if ( world->locked )
	{
		return treeStats;
	}

	B2_ASSERT( b2IsValidPos( origin ) );
	B2_ASSERT( b2IsValidAABB( aabb ) );

	b2RecQueryWriter recWriter = { 0 };
	if ( world->recording != NULL )
	{
		b2RecQueryBegin( &recWriter, context );
		recWriter.userFcn.overlapFcn = fcn;
		b2RecW_WORLDID( &recWriter.buf, worldId );
		b2RecW_POSITION( &recWriter.buf, origin );
		b2RecW_AABB( &recWriter.buf, aabb );
		b2RecW_QUERYFILTER( &recWriter.buf, filter );
		recWriter.countOffset = b2RecReserveU32( &recWriter.buf );
		fcn = b2RecOverlapTrampoline;
		context = &recWriter;
	}

	// Lift to a world float box with outward rounding so the conservative tree test never misses
	b2AABB worldBox = b2OffsetAABB( aabb, origin );

	WorldQueryContext worldContext = { world, fcn, filter, context };

	for ( int i = 0; i < b2_bodyTypeCount; ++i )
	{
		b2TreeStats treeResult =
			b2DynamicTree_OverlapAABB( world->broadPhase.trees + i, worldBox, filter.maskBits, TreeQueryCallback, &worldContext );

		treeStats.nodeVisits += treeResult.nodeVisits;
		treeStats.leafVisits += treeResult.leafVisits;
	}

	if ( world->recording != NULL )
	{
		b2RecPatchU32( &recWriter.buf, recWriter.countOffset, recWriter.hitCount );
		b2RecW_TREESTATS( &recWriter.buf, treeStats );
		b2RecQueryCommit( world->recording, 0xE0, &recWriter );
	}

	return treeStats;
}

typedef struct WorldOverlapContext
{
	b2World* world;
	b2OverlapResultCallbackFcn* fcn;
	b2QueryFilter filter;
	const b2ShapeProxy* proxy;
	b2Pos origin;
	void* userContext;
} WorldOverlapContext;

static bool TreeOverlapCallback( int proxyId, uint64_t userData, void* context )
{
	B2_UNUSED( proxyId );

	int shapeId = (int)userData;

	WorldOverlapContext* worldContext = context;
	b2World* world = worldContext->world;

	b2Shape* shape = b2Array_Get( world->shapes, shapeId );

	if ( b2ShouldQueryCollide( shape->filter, worldContext->filter ) == false )
	{
		return true;
	}

	// Re-center on the query origin so the distance test stays in float precision far from the world origin
	b2Body* body = b2Array_Get( world->bodies, shape->bodyId );
	b2Transform transform = b2ToRelativeTransform( b2GetBodyTransformQuick( world, body ), worldContext->origin );

	b2DistanceInput input;
	input.proxyA = *worldContext->proxy;
	input.proxyB = b2MakeShapeDistanceProxy( shape );
	input.transform = transform;
	input.useRadii = true;

	b2SimplexCache cache = { 0 };
	b2DistanceOutput output = b2ComputeShapeDistance( &input, &cache, NULL, 0 );

	float tolerance = 0.1f * B2_LINEAR_SLOP;
	if ( output.distance > tolerance )
	{
		return true;
	}

	b2ShapeId id = { shape->id + 1, world->worldId, shape->generation };
	bool result = worldContext->fcn( id, worldContext->userContext );
	return result;
}

b2TreeStats b2World_OverlapShape( b2WorldId worldId, b2Pos origin, const b2ShapeProxy* proxy, b2QueryFilter filter,
								  b2OverlapResultCallbackFcn* fcn, void* context )
{
	b2TreeStats treeStats = { 0 };

	b2World* world = b2GetWorldFromId( worldId );
	B2_ASSERT( world->locked == false );
	if ( world->locked )
	{
		return treeStats;
	}

	B2_ASSERT( b2IsValidPos( origin ) );

	b2RecQueryWriter recWriter = { 0 };
	if ( world->recording != NULL )
	{
		b2RecQueryBegin( &recWriter, context );
		recWriter.userFcn.overlapFcn = fcn;
		b2RecW_WORLDID( &recWriter.buf, worldId );
		b2RecW_POSITION( &recWriter.buf, origin );
		b2RecW_SHAPEPROXY( &recWriter.buf, *proxy );
		b2RecW_QUERYFILTER( &recWriter.buf, filter );
		recWriter.countOffset = b2RecReserveU32( &recWriter.buf );
		fcn = b2RecOverlapTrampoline;
		context = &recWriter;
	}

	// Relative box lifted to world float with outward rounding, conservative for the tree
	b2AABB aabb = b2OffsetAABB( b2MakeAABB( proxy->points, proxy->count, proxy->radius ), origin );
	WorldOverlapContext worldContext = {
		world, fcn, filter, proxy, origin, context,
	};

	for ( int i = 0; i < b2_bodyTypeCount; ++i )
	{
		b2TreeStats treeResult =
			b2DynamicTree_OverlapAABB( world->broadPhase.trees + i, aabb, filter.maskBits, TreeOverlapCallback, &worldContext );

		treeStats.nodeVisits += treeResult.nodeVisits;
		treeStats.leafVisits += treeResult.leafVisits;
	}

	if ( world->recording != NULL )
	{
		b2RecPatchU32( &recWriter.buf, recWriter.countOffset, recWriter.hitCount );
		b2RecW_TREESTATS( &recWriter.buf, treeStats );
		b2RecQueryCommit( world->recording, 0xE1, &recWriter );
	}

	return treeStats;
}

typedef struct WorldRayCastContext
{
	b2World* world;
	b2CastResultCallbackFcn* fcn;
	b2QueryFilter filter;
	float fraction;
	b2Pos origin;
	void* userContext;
} WorldRayCastContext;

static float RayCastCallback( const b2RayCastInput* input, int proxyId, uint64_t userData, void* context )
{
	B2_UNUSED( proxyId );

	int shapeId = (int)userData;

	WorldRayCastContext* worldContext = context;
	b2World* world = worldContext->world;

	b2Shape* shape = b2Array_Get( world->shapes, shapeId );

	if ( b2ShouldQueryCollide( shape->filter, worldContext->filter ) == false )
	{
		return input->maxFraction;
	}

	b2Body* body = b2Array_Get( world->bodies, shape->bodyId );
	b2WorldTransform bodyTransform = b2GetBodyTransformQuick( world, body );
	b2Transform transform = b2ToRelativeTransform( bodyTransform, worldContext->origin );

	b2RayCastInput localInput = *input;
	localInput.origin = b2Vec2_zero;
	b2CastOutput output = b2RayCastShape( &localInput, shape, transform );

	if ( output.hit )
	{
		b2ShapeId id = { shapeId + 1, world->worldId, shape->generation };
		b2Pos point = b2OffsetPos( worldContext->origin, output.point );
		float fraction = worldContext->fcn( id, point, output.normal, output.fraction, worldContext->userContext );

		// The user may return -1 to skip this shape
		if ( 0.0f <= fraction && fraction <= 1.0f )
		{
			worldContext->fraction = fraction;
		}

		return fraction;
	}

	return input->maxFraction;
}

b2TreeStats b2World_CastRay( b2WorldId worldId, b2Pos origin, b2Vec2 translation, b2QueryFilter filter, b2CastResultCallbackFcn* fcn,
							 void* context )
{
	b2TreeStats treeStats = { 0 };

	b2World* world = b2GetWorldFromId( worldId );
	B2_ASSERT( world->locked == false );
	if ( world->locked )
	{
		return treeStats;
	}

	B2_ASSERT( b2IsValidPos( origin ) );
	B2_ASSERT( b2IsValidVec2( translation ) );

	b2RecQueryWriter recWriter = { 0 };
	if ( world->recording != NULL )
	{
		b2RecQueryBegin( &recWriter, context );
		recWriter.userFcn.castFcn = fcn;
		b2RecW_WORLDID( &recWriter.buf, worldId );
		b2RecW_POSITION( &recWriter.buf, origin );
		b2RecW_VEC2( &recWriter.buf, translation );
		b2RecW_QUERYFILTER( &recWriter.buf, filter );
		recWriter.countOffset = b2RecReserveU32( &recWriter.buf );
		fcn = b2RecCastTrampoline;
		context = &recWriter;
	}

	// Tree traversal sees the origin truncated to float, displacing the ray by up to one
	// coordinate ULP, a graze sized miss tolerance at extreme range. Per-shape casts
	// re-difference against the full precision origin carried on the context.
	b2RayCastInput input = { b2ToVec2( origin ), translation, 1.0f };

	WorldRayCastContext worldContext = { world, fcn, filter, 1.0f, origin, context };

	for ( int i = 0; i < b2_bodyTypeCount; ++i )
	{
		b2TreeStats treeResult =
			b2DynamicTree_CastRay( world->broadPhase.trees + i, &input, filter.maskBits, RayCastCallback, &worldContext );
		treeStats.nodeVisits += treeResult.nodeVisits;
		treeStats.leafVisits += treeResult.leafVisits;

		if ( worldContext.fraction == 0.0f )
		{
			break;
		}

		input.maxFraction = worldContext.fraction;
	}

	if ( world->recording != NULL )
	{
		b2RecPatchU32( &recWriter.buf, recWriter.countOffset, recWriter.hitCount );
		b2RecW_TREESTATS( &recWriter.buf, treeStats );
		b2RecQueryCommit( world->recording, 0xE2, &recWriter );
	}

	return treeStats;
}

// This callback finds the closest hit. This is the most common callback used in games.
static float b2RayCastClosestFcn( b2ShapeId shapeId, b2Pos point, b2Vec2 normal, float fraction, void* context )
{
	// Ignore initial overlap
	if ( fraction == 0.0f )
	{
		return -1.0f;
	}

	b2RayResult* rayResult = (b2RayResult*)context;
	rayResult->shapeId = shapeId;
	rayResult->point = point;
	rayResult->normal = normal;
	rayResult->fraction = fraction;
	rayResult->hit = true;
	return fraction;
}

b2RayResult b2World_CastRayClosest( b2WorldId worldId, b2Pos origin, b2Vec2 translation, b2QueryFilter filter )
{
	b2RayResult result = { 0 };

	b2World* world = b2GetWorldFromId( worldId );
	B2_ASSERT( world->locked == false );
	if ( world->locked )
	{
		return result;
	}

	B2_ASSERT( b2IsValidPos( origin ) );
	B2_ASSERT( b2IsValidVec2( translation ) );

	b2RayCastInput input = { b2ToVec2( origin ), translation, 1.0f };
	WorldRayCastContext worldContext = { world, b2RayCastClosestFcn, filter, 1.0f, origin, &result };

	for ( int i = 0; i < b2_bodyTypeCount; ++i )
	{
		b2TreeStats treeResult =
			b2DynamicTree_CastRay( world->broadPhase.trees + i, &input, filter.maskBits, RayCastCallback, &worldContext );
		result.nodeVisits += treeResult.nodeVisits;
		result.leafVisits += treeResult.leafVisits;

		if ( worldContext.fraction == 0.0f )
		{
			break;
		}

		input.maxFraction = worldContext.fraction;
	}

	if ( world->recording != NULL )
	{
		b2RecBuffer recBuf = { 0 };
		b2RecW_WORLDID( &recBuf, worldId );
		b2RecW_POSITION( &recBuf, origin );
		b2RecW_VEC2( &recBuf, translation );
		b2RecW_QUERYFILTER( &recBuf, filter );
		b2RecW_RAYRESULT( &recBuf, result );
		b2RecCommitRecord( world->recording, 0xE5, recBuf.data, recBuf.size );
		b2RecBufFree( &recBuf );
	}

	return result;
}

typedef struct WorldShapeCastContext
{
	b2World* world;
	b2CastResultCallbackFcn* fcn;
	b2QueryFilter filter;
	float fraction;
	b2Pos origin;
	// origin relative input
	b2ShapeCastInput input;
	void* userContext;
} WorldShapeCastContext;

static float ShapeCastCallback( const b2BoxCastInput* input, int proxyId, uint64_t userData, void* context )
{
	B2_UNUSED( proxyId );

	int shapeId = (int)userData;

	WorldShapeCastContext* worldContext = context;
	b2World* world = worldContext->world;

	b2Shape* shape = b2Array_Get( world->shapes, shapeId );

	if ( b2ShouldQueryCollide( shape->filter, worldContext->filter ) == false )
	{
		return input->maxFraction;
	}

	// Rebuild from the origin relative input, taking only the advancing fraction from the tree.
	// The tree input is world float and would lose the cast far from the origin.
	b2ShapeCastInput localInput = worldContext->input;
	localInput.maxFraction = input->maxFraction;

	b2Body* body = b2Array_Get( world->bodies, shape->bodyId );
	b2WorldTransform transform = b2GetBodyTransformQuick( world, body );
	b2Transform localTransform = b2ToRelativeTransform( transform, worldContext->origin );

	b2CastOutput output = b2ShapeCastShape( &localInput, shape, localTransform );

	if ( output.hit )
	{
		b2ShapeId id = { shapeId + 1, world->worldId, shape->generation };

		b2Pos point = b2OffsetPos( worldContext->origin, output.point );
		float fraction = worldContext->fcn( id, point, output.normal, output.fraction, worldContext->userContext );

		// The user may return -1 to skip this shape
		if ( 0.0f <= fraction && fraction <= 1.0f )
		{
			worldContext->fraction = fraction;
		}

		return fraction;
	}

	return input->maxFraction;
}

b2TreeStats b2World_CastShape( b2WorldId worldId, b2Pos origin, const b2ShapeProxy* proxy, b2Vec2 translation,
							   b2QueryFilter filter, b2CastResultCallbackFcn* fcn, void* context )
{
	b2TreeStats treeStats = { 0 };

	b2World* world = b2GetWorldFromId( worldId );
	B2_ASSERT( world->locked == false );
	if ( world->locked )
	{
		return treeStats;
	}

	B2_ASSERT( b2IsValidPos( origin ) );
	B2_ASSERT( b2IsValidVec2( translation ) );

	b2RecQueryWriter recWriter = { 0 };
	if ( world->recording != NULL )
	{
		b2RecQueryBegin( &recWriter, context );
		recWriter.userFcn.castFcn = fcn;
		b2RecW_WORLDID( &recWriter.buf, worldId );
		b2RecW_POSITION( &recWriter.buf, origin );
		b2RecW_SHAPEPROXY( &recWriter.buf, *proxy );
		b2RecW_VEC2( &recWriter.buf, translation );
		b2RecW_QUERYFILTER( &recWriter.buf, filter );
		recWriter.countOffset = b2RecReserveU32( &recWriter.buf );
		fcn = b2RecCastTrampoline;
		context = &recWriter;
	}

	WorldShapeCastContext worldContext = { 0 };
	worldContext.world = world;
	worldContext.fcn = fcn;
	worldContext.filter = filter;
	worldContext.fraction = 1.0f;
	worldContext.origin = origin;
	worldContext.input.proxy = *proxy;
	worldContext.input.translation = translation;
	worldContext.input.maxFraction = 1.0f;
	worldContext.userContext = context;

	// Create a conservative world space box from the proxy and origin.
	b2AABB localBox = b2MakeAABB( proxy->points, proxy->count, proxy->radius );
	b2AABB box = b2OffsetAABB( localBox, origin );
	b2BoxCastInput treeInput = { box, translation, 1.0f };

	for ( int i = 0; i < b2_bodyTypeCount; ++i )
	{
		b2TreeStats treeResult =
			b2DynamicTree_CastBox( world->broadPhase.trees + i, &treeInput, filter.maskBits, ShapeCastCallback, &worldContext );
		treeStats.nodeVisits += treeResult.nodeVisits;
		treeStats.leafVisits += treeResult.leafVisits;

		if ( worldContext.fraction == 0.0f )
		{
			break;
		}

		treeInput.maxFraction = worldContext.fraction;
	}

	if ( world->recording != NULL )
	{
		b2RecPatchU32( &recWriter.buf, recWriter.countOffset, recWriter.hitCount );
		b2RecW_TREESTATS( &recWriter.buf, treeStats );
		b2RecQueryCommit( world->recording, 0xE3, &recWriter );
	}

	return treeStats;
}

typedef struct WorldMoverCastContext
{
	b2World* world;
	b2QueryFilter filter;
	float fraction;
	b2Pos origin;
	// origin relative input
	b2ShapeCastInput input;
} WorldMoverCastContext;

static float MoverCastCallback( const b2BoxCastInput* input, int proxyId, uint64_t userData, void* context )
{
	B2_UNUSED( proxyId );

	int shapeId = (int)userData;
	WorldMoverCastContext* worldContext = context;
	b2World* world = worldContext->world;

	b2Shape* shape = b2Array_Get( world->shapes, shapeId );

	if ( b2ShouldQueryCollide( shape->filter, worldContext->filter ) == false )
	{
		return worldContext->fraction;
	}

	// Rebuild from the origin relative input, taking only the advancing fraction from the tree
	b2ShapeCastInput localInput = worldContext->input;
	localInput.maxFraction = input->maxFraction;

	b2Body* body = b2Array_Get( world->bodies, shape->bodyId );
	b2WorldTransform bodyTransform = b2GetBodyTransformQuick( world, body );
	b2Transform transform = b2ToRelativeTransform( bodyTransform, worldContext->origin );

	b2CastOutput output = b2ShapeCastShape( &localInput, shape, transform );
	if ( output.fraction == 0.0f )
	{
		// Ignore overlapping shapes
		return worldContext->fraction;
	}

	worldContext->fraction = output.fraction;
	return output.fraction;
}

float b2World_CastMover( b2WorldId worldId, b2Pos origin, const b2Capsule* mover, b2Vec2 translation, b2QueryFilter filter )
{
	B2_ASSERT( b2IsValidPos( origin ) );
	B2_ASSERT( b2IsValidVec2( translation ) );
	B2_ASSERT( mover->radius > 2.0f * B2_LINEAR_SLOP );

	b2World* world = b2GetWorldFromId( worldId );
	B2_ASSERT( world->locked == false );
	if ( world->locked )
	{
		return 1.0f;
	}

	WorldMoverCastContext worldContext = { 0 };
	worldContext.world = world;
	worldContext.filter = filter;
	worldContext.fraction = 1.0f;
	worldContext.origin = origin;
	worldContext.input.proxy.points[0] = mover->center1;
	worldContext.input.proxy.points[1] = mover->center2;
	worldContext.input.proxy.count = 2;
	worldContext.input.proxy.radius = mover->radius;
	worldContext.input.translation = translation;
	worldContext.input.maxFraction = 1.0f;
	worldContext.input.canEncroach = true;

	// Bound the capsule in origin relative space then lift to a conservative world float box
	b2Vec2 centers[2] = { mover->center1, mover->center2 };
	b2AABB box = b2OffsetAABB( b2MakeAABB( centers, 2, mover->radius ), origin );
	b2BoxCastInput treeInput = { box, translation, 1.0f };

	for ( int i = 0; i < b2_bodyTypeCount; ++i )
	{
		b2DynamicTree_CastBox( world->broadPhase.trees + i, &treeInput, filter.maskBits, MoverCastCallback, &worldContext );

		if ( worldContext.fraction == 0.0f )
		{
			break;
		}

		treeInput.maxFraction = worldContext.fraction;
	}

	if ( world->recording != NULL )
	{
		b2RecBuffer recBuf = { 0 };
		b2RecW_WORLDID( &recBuf, worldId );
		b2RecW_POSITION( &recBuf, origin );
		b2RecW_CAPSULE( &recBuf, *mover );
		b2RecW_VEC2( &recBuf, translation );
		b2RecW_QUERYFILTER( &recBuf, filter );
		b2RecW_F32( &recBuf, worldContext.fraction );
		b2RecCommitRecord( world->recording, 0xE6, recBuf.data, recBuf.size );
		b2RecBufFree( &recBuf );
	}

	return worldContext.fraction;
}

typedef struct WorldMoverContext
{
	b2World* world;
	b2PlaneResultCallbackFcn* fcn;
	b2QueryFilter filter;
	b2Capsule mover;
	b2Pos origin;
	void* userContext;
} WorldMoverContext;

static bool TreeCollideCallback( int proxyId, uint64_t userData, void* context )
{
	B2_UNUSED( proxyId );

	int shapeId = (int)userData;
	WorldMoverContext* worldContext = (WorldMoverContext*)context;
	b2World* world = worldContext->world;

	b2Shape* shape = b2Array_Get( world->shapes, shapeId );

	if ( b2ShouldQueryCollide( shape->filter, worldContext->filter ) == false )
	{
		return true;
	}

	// Re-center on the query origin, the mover and the resulting planes are origin relative
	b2Body* body = b2Array_Get( world->bodies, shape->bodyId );
	b2Transform transform = b2ToRelativeTransform( b2GetBodyTransformQuick( world, body ), worldContext->origin );

	b2PlaneResult result = b2CollideMover( &worldContext->mover, shape, transform );

	// todo handle deep overlap
	if ( result.hit && b2IsNormalized( result.plane.normal ) )
	{
		b2ShapeId id = { shape->id + 1, world->worldId, shape->generation };
		return worldContext->fcn( id, &result, worldContext->userContext );
	}

	return true;
}

// It is tempting to use a shape proxy for the mover, but this makes handling deep overlap difficult and the generality may
// not be worth it.
void b2World_CollideMover( b2WorldId worldId, b2Pos origin, const b2Capsule* mover, b2QueryFilter filter, b2PlaneResultCallbackFcn* fcn,
						   void* context )
{
	b2World* world = b2GetWorldFromId( worldId );
	B2_ASSERT( world->locked == false );
	if ( world->locked )
	{
		return;
	}

	B2_ASSERT( b2IsValidPos( origin ) );

	b2RecQueryWriter recWriter = { 0 };
	if ( world->recording != NULL )
	{
		b2RecQueryBegin( &recWriter, context );
		recWriter.userFcn.planeFcn = fcn;
		b2RecW_WORLDID( &recWriter.buf, worldId );
		b2RecW_POSITION( &recWriter.buf, origin );
		b2RecW_CAPSULE( &recWriter.buf, *mover );
		b2RecW_QUERYFILTER( &recWriter.buf, filter );
		recWriter.countOffset = b2RecReserveU32( &recWriter.buf );
		fcn = b2RecPlaneTrampoline;
		context = &recWriter;
	}

	b2Vec2 r = { mover->radius, mover->radius };

	// Relative box lifted to world float with outward rounding, conservative for the tree
	b2AABB relBox;
	relBox.lowerBound = b2Sub( b2Min( mover->center1, mover->center2 ), r );
	relBox.upperBound = b2Add( b2Max( mover->center1, mover->center2 ), r );
	b2AABB aabb = b2OffsetAABB( relBox, origin );

	WorldMoverContext worldContext = {
		world, fcn, filter, *mover, origin, context,
	};

	for ( int i = 0; i < b2_bodyTypeCount; ++i )
	{
		b2DynamicTree_OverlapAABB( world->broadPhase.trees + i, aabb, filter.maskBits, TreeCollideCallback, &worldContext );
	}

	if ( world->recording != NULL )
	{
		b2RecPatchU32( &recWriter.buf, recWriter.countOffset, recWriter.hitCount );
		// CollideMover returns void; no TREESTATS tail
		b2RecQueryCommit( world->recording, 0xE4, &recWriter );
	}
}

void b2World_SetCustomFilterCallback( b2WorldId worldId, b2CustomFilterCallbackFcn* fcn, void* context )
{
	b2World* world = b2GetWorldFromId( worldId );
	if ( fcn != NULL && world->recording != NULL )
	{
		printf( "b2World_SetCustomFilterCallback: customFilter not supported while recording\n" );
		B2_ASSERT( false && "customFilter callbacks are not supported while recording" );
		return;
	}

	world->customFilterFcn = fcn;
	world->customFilterContext = context;
}

void b2World_SetPreSolveCallback( b2WorldId worldId, b2PreSolveCallbackFcn* preSolveFcn, b2PreContinuousCallbackFcn* preContinuousFcn,
								  void* context )
{
	b2World* world = b2GetWorldFromId( worldId );
	if ( ( preSolveFcn != NULL || preContinuousFcn != NULL ) && world->recording != NULL )
	{
		printf( "b2World_SetPreSolveCallback: preSolve not supported while recording\n" );
		B2_ASSERT( false && "preSolve callbacks are not supported while recording" );
		return;
	}

	world->preSolveFcn = preSolveFcn;
	world->preContinuousFcn = preContinuousFcn;
	world->preSolveContext = context;
}

void b2World_SetGravity( b2WorldId worldId, b2Vec2 gravity )
{
	B2_CHECK_INPUT( b2IsValidVec2( gravity ) );

	b2World* world = b2GetWorldFromId( worldId );
	B2_REC( world, WorldSetGravity, worldId, gravity );
	world->gravity = gravity;
}

b2Vec2 b2World_GetGravity( b2WorldId worldId )
{
	b2World* world = b2GetWorldFromId( worldId );
	return world->gravity;
}

struct ExplosionContext
{
	b2World* world;
	b2Pos position;
	float radius;
	float falloff;
	float impulsePerLength;
};

static bool ExplosionCallback( int proxyId, uint64_t userData, void* context )
{
	B2_UNUSED( proxyId );

	int shapeId = (int)userData;
	struct ExplosionContext* explosionContext = context;
	b2World* world = explosionContext->world;

	b2Shape* shape = b2Array_Get( world->shapes, shapeId );

	b2Body* body = b2Array_Get( world->bodies, shape->bodyId );
	B2_ASSERT( body->type == b2_dynamicBody );

	b2WorldTransform xf = b2GetBodyTransformQuick( world, body );

	// Re-center the explosion into the shape local frame so distance and direction stay precise
	// far from the origin. Everything below runs in that near-origin frame.
	b2Vec2 localPosition = b2InvTransformWorldPoint( xf, explosionContext->position );

	b2DistanceInput input;
	input.proxyA = b2MakeShapeDistanceProxy( shape );
	input.proxyB = b2MakeProxy( &localPosition, 1, 0.0f );
	input.transform = b2Transform_identity;
	input.useRadii = true;

	b2SimplexCache cache = { 0 };
	b2DistanceOutput output = b2ComputeShapeDistance( &input, &cache, NULL, 0 );

	float radius = explosionContext->radius;
	float falloff = explosionContext->falloff;
	if ( output.distance > radius + falloff )
	{
		return true;
	}

	b2WakeBody( world, body );

	if ( body->setIndex != b2_awakeSet )
	{
		return true;
	}

	b2Vec2 closestPoint = output.pointA;
	if ( output.distance == 0.0f )
	{
		closestPoint = b2GetShapeCentroid( shape );
	}

	b2Vec2 direction = b2Sub( closestPoint, localPosition );
	if ( b2LengthSquared( direction ) > 100.0f * FLT_EPSILON * FLT_EPSILON )
	{
		direction = b2Normalize( direction );
	}
	else
	{
		direction = (b2Vec2){ 1.0f, 0.0f };
	}

	b2Vec2 localLine = b2LeftPerp( direction );
	float perimeter = b2GetShapeProjectedPerimeter( shape, localLine );
	float scale = 1.0f;
	if ( output.distance > radius && falloff > 0.0f )
	{
		scale = b2ClampFloat( ( radius + falloff - output.distance ) / falloff, 0.0f, 1.0f );
	}

	float magnitude = explosionContext->impulsePerLength * perimeter * scale;
	b2Vec2 impulse = b2MulSV( magnitude, b2RotateVector( xf.q, direction ) );

	int localIndex = body->localIndex;
	b2SolverSet* set = b2Array_Get( world->solverSets, b2_awakeSet );
	b2BodyState* state = b2Array_Get( set->bodyStates, localIndex );
	b2BodySim* bodySim = b2Array_Get( set->bodySims, localIndex );
	state->linearVelocity = b2MulAdd( state->linearVelocity, bodySim->invMass, impulse );

	// Lever arm from the center of mass to the closest point, rotated to world
	b2Vec2 r = b2RotateVector( xf.q, b2Sub( closestPoint, bodySim->localCenter ) );
	state->angularVelocity += bodySim->invInertia * b2Cross( r, impulse );

	return true;
}

void b2World_Explode( b2WorldId worldId, const b2ExplosionDef* explosionDef )
{
	uint64_t maskBits = explosionDef->maskBits;
	b2Pos position = explosionDef->position;
	float radius = explosionDef->radius;
	float falloff = explosionDef->falloff;
	float impulsePerLength = explosionDef->impulsePerLength;

	B2_CHECK_INPUT( b2IsValidPos( position ) );
	B2_CHECK_INPUT( b2IsValidFloat( radius ) && radius >= 0.0f );
	B2_CHECK_INPUT( b2IsValidFloat( falloff ) && falloff >= 0.0f );
	B2_CHECK_INPUT( b2IsValidFloat( impulsePerLength ) );

	b2World* world = b2GetWorldFromId( worldId );
	B2_ASSERT( world->locked == false );
	if ( world->locked )
	{
		return;
	}

	B2_REC( world, WorldExplode, worldId, *explosionDef );

	struct ExplosionContext explosionContext = { world, position, radius, falloff, impulsePerLength };

	// The broad-phase tree is float, so translate a local query box out to world with outward rounding
	float extent = radius + falloff;
	b2AABB localBox = { { -extent, -extent }, { extent, extent } };
	b2AABB aabb = b2OffsetAABB( localBox, position );

	b2DynamicTree_OverlapAABB( world->broadPhase.trees + b2_dynamicBody, aabb, maskBits, ExplosionCallback, &explosionContext );
}

// This is for internal testing. Not optimized.
void b2World_RebuildStaticTree( b2WorldId worldId )
{
	b2World* world = b2GetWorldFromId( worldId );
	B2_ASSERT( world->locked == false );
	if ( world->locked )
	{
		return;
	}

	B2_REC( world, WorldRebuildStaticTree, worldId );

	b2DynamicTree* staticTree = world->broadPhase.trees + b2_staticBody;

	// Moved proxies must retain the flag in the rebuild.
	int movedCount = 0;
	int* movedProxies = NULL;
	int proxyCount = b2DynamicTree_GetProxyCount( staticTree );
	if ( b2HasTreeMoved( staticTree ) )
	{
		movedProxies = b2Alloc( proxyCount * sizeof( int ) );
		movedCount = b2DynamicTree_GatherMovedProxies( staticTree, movedProxies );
	}

	b2DynamicTree_Rebuild( staticTree, true );

	for ( int i = 0; i < movedCount; ++i )
	{
		b2DynamicTree_MarkProxyMovedSerial( staticTree, movedProxies[i] );
	}

	if ( movedProxies != NULL )
	{
		b2Free( movedProxies, proxyCount * sizeof( int ) );
	}
}

#if B2_ENABLE_VALIDATION
// This validates island graph connectivity for each body
void b2ValidateConnectivity( b2World* world )
{
	b2Body* bodies = world->bodies.data;
	int bodyCapacity = world->bodies.count;

	for ( int bodyIndex = 0; bodyIndex < bodyCapacity; ++bodyIndex )
	{
		b2Body* body = bodies + bodyIndex;
		if ( body->id == B2_NULL_INDEX )
		{
			b2ValidateFreeId( &world->bodyIdPool, bodyIndex );
			continue;
		}

		b2ValidateUsedId( &world->bodyIdPool, bodyIndex );

		B2_ASSERT( bodyIndex == body->id );

		// Need to get the root island because islands are not merged until the next time step
		int bodyIslandId = body->islandId;
		int bodySetIndex = body->setIndex;

		int contactKey = body->headContactKey;
		while ( contactKey != B2_NULL_INDEX )
		{
			int contactId = contactKey >> 1;
			int edgeIndex = contactKey & 1;

			b2Contact* contact = b2Array_Get( world->contacts, contactId );

			bool touching = ( contact->flags & b2_contactTouchingFlag ) != 0;
			if ( touching )
			{
				if ( bodySetIndex != b2_staticSet )
				{
					int contactIslandId = contact->islandId;
					B2_ASSERT( contactIslandId == bodyIslandId );
				}
			}
			else
			{
				B2_ASSERT( contact->islandId == B2_NULL_INDEX );
			}

			contactKey = contact->edges[edgeIndex].nextKey;
		}

		int jointKey = body->headJointKey;
		while ( jointKey != B2_NULL_INDEX )
		{
			int jointId = jointKey >> 1;
			int edgeIndex = jointKey & 1;

			b2Joint* joint = b2Array_Get( world->joints, jointId );

			int otherEdgeIndex = edgeIndex ^ 1;

			b2Body* otherBody = b2Array_Get( world->bodies, joint->edges[otherEdgeIndex].bodyId );

			if ( bodySetIndex == b2_disabledSet || otherBody->setIndex == b2_disabledSet )
			{
				B2_ASSERT( joint->islandId == B2_NULL_INDEX );
			}
			else if ( bodySetIndex == b2_staticSet )
			{
				// Intentional nesting
				if ( otherBody->setIndex == b2_staticSet )
				{
					B2_ASSERT( joint->islandId == B2_NULL_INDEX );
				}
			}
			else if ( body->type != b2_dynamicBody && otherBody->type != b2_dynamicBody )
			{
				B2_ASSERT( joint->islandId == B2_NULL_INDEX );
			}
			else
			{
				int jointIslandId = joint->islandId;
				B2_ASSERT( jointIslandId == bodyIslandId );
			}

			jointKey = joint->edges[edgeIndex].nextKey;
		}
	}
}

// Validates solver sets, but not island connectivity
void b2ValidateSolverSets( b2World* world )
{
	B2_ASSERT( b2GetIdCapacity( &world->bodyIdPool ) == world->bodies.count );
	B2_ASSERT( b2GetIdCapacity( &world->contactIdPool ) == world->contacts.count );
	B2_ASSERT( b2GetIdCapacity( &world->jointIdPool ) == world->joints.count );
	B2_ASSERT( b2GetIdCapacity( &world->islandIdPool ) == world->islands.count );
	B2_ASSERT( b2GetIdCapacity( &world->solverSetIdPool ) == world->solverSets.count );

	int activeSetCount = 0;
	int totalBodyCount = 0;
	int totalJointCount = 0;
	int totalContactCount = 0;
	int totalIslandCount = 0;

	// Validate all solver sets
	int setCount = world->solverSets.count;
	for ( int setIndex = 0; setIndex < setCount; ++setIndex )
	{
		b2SolverSet* set = world->solverSets.data + setIndex;
		if ( set->setIndex != B2_NULL_INDEX )
		{
			activeSetCount += 1;

			if ( setIndex == b2_staticSet )
			{
				B2_ASSERT( set->contactSims.count == 0 );
				B2_ASSERT( set->islandSims.count == 0 );
				B2_ASSERT( set->bodyStates.count == 0 );
			}
			else if ( setIndex == b2_disabledSet )
			{
				B2_ASSERT( set->islandSims.count == 0 );
				B2_ASSERT( set->bodyStates.count == 0 );
			}
			else if ( setIndex == b2_awakeSet )
			{
				B2_ASSERT( set->bodySims.count == set->bodyStates.count );
				B2_ASSERT( set->jointSims.count == 0 );
			}
			else
			{
				B2_ASSERT( set->bodyStates.count == 0 );
			}

			// Validate bodies
			{
				b2Body* bodies = world->bodies.data;
				B2_ASSERT( set->bodySims.count >= 0 );
				totalBodyCount += set->bodySims.count;
				for ( int i = 0; i < set->bodySims.count; ++i )
				{
					b2BodySim* bodySim = set->bodySims.data + i;

					int bodyId = bodySim->bodyId;
					B2_ASSERT( 0 <= bodyId && bodyId < world->bodies.count );
					b2Body* body = bodies + bodyId;
					B2_ASSERT( body->setIndex == setIndex );
					B2_ASSERT( body->localIndex == i );

					uint32_t syncedFlags = body->flags & ~( b2_isFast | b2_bodyTransientFlags );
					B2_ASSERT( ( bodySim->flags & syncedFlags ) == syncedFlags );

					b2BodyState* bodyState = b2GetBodyState( world, body );
					if ( bodyState != NULL )
					{
						B2_ASSERT( ( bodyState->flags & syncedFlags ) == syncedFlags );
					}

					if ( body->type == b2_dynamicBody )
					{
						B2_ASSERT( body->flags & b2_dynamicFlag );
					}

					if ( setIndex == b2_disabledSet )
					{
						B2_ASSERT( body->headContactKey == B2_NULL_INDEX );
					}

					// Validate body shapes
					int prevShapeId = B2_NULL_INDEX;
					int shapeId = body->headShapeId;
					while ( shapeId != B2_NULL_INDEX )
					{
						b2Shape* shape = b2Array_Get( world->shapes, shapeId );
						B2_ASSERT( shape->id == shapeId );
						B2_ASSERT( shape->prevShapeId == prevShapeId );

						if ( setIndex == b2_disabledSet )
						{
							B2_ASSERT( shape->proxyKey == B2_NULL_INDEX );
						}
						else if ( setIndex == b2_staticSet )
						{
							B2_ASSERT( B2_PROXY_TYPE( shape->proxyKey ) == b2_staticBody );
						}
						else
						{
							b2BodyType proxyType = B2_PROXY_TYPE( shape->proxyKey );
							B2_ASSERT( proxyType == b2_kinematicBody || proxyType == b2_dynamicBody );
						}

						prevShapeId = shapeId;
						shapeId = shape->nextShapeId;
					}

					// Validate body contacts
					int contactKey = body->headContactKey;
					while ( contactKey != B2_NULL_INDEX )
					{
						int contactId = contactKey >> 1;
						int edgeIndex = contactKey & 1;

						b2Contact* contact = b2Array_Get( world->contacts, contactId );
						B2_ASSERT( contact->setIndex != b2_staticSet );
						B2_ASSERT( contact->edges[0].bodyId == bodyId || contact->edges[1].bodyId == bodyId );
						contactKey = contact->edges[edgeIndex].nextKey;
					}

					// Validate body joints
					int jointKey = body->headJointKey;
					while ( jointKey != B2_NULL_INDEX )
					{
						int jointId = jointKey >> 1;
						int edgeIndex = jointKey & 1;

						b2Joint* joint = b2Array_Get( world->joints, jointId );

						int otherEdgeIndex = edgeIndex ^ 1;

						b2Body* otherBody = b2Array_Get( world->bodies, joint->edges[otherEdgeIndex].bodyId );

						if ( setIndex == b2_disabledSet || otherBody->setIndex == b2_disabledSet )
						{
							B2_ASSERT( joint->setIndex == b2_disabledSet );
						}
						else if ( setIndex == b2_staticSet && otherBody->setIndex == b2_staticSet )
						{
							B2_ASSERT( joint->setIndex == b2_staticSet );
						}
						else if ( body->type != b2_dynamicBody && otherBody->type != b2_dynamicBody )
						{
							B2_ASSERT( joint->setIndex == b2_staticSet );
						}
						else if ( setIndex == b2_awakeSet )
						{
							B2_ASSERT( joint->setIndex == b2_awakeSet );
						}
						else if ( setIndex >= b2_firstSleepingSet )
						{
							B2_ASSERT( joint->setIndex == setIndex );
						}

						b2JointSim* jointSim = b2GetJointSim( world, joint );
						B2_ASSERT( jointSim->jointId == jointId );
						B2_ASSERT( jointSim->bodyIdA == joint->edges[0].bodyId );
						B2_ASSERT( jointSim->bodyIdB == joint->edges[1].bodyId );

						jointKey = joint->edges[edgeIndex].nextKey;
					}
				}
			}

			// Validate contacts
			{
				B2_ASSERT( set->contactSims.count >= 0 );
				totalContactCount += set->contactSims.count;
				for ( int i = 0; i < set->contactSims.count; ++i )
				{
					b2ContactSim* contactSim = set->contactSims.data + i;
					b2Contact* contact = b2Array_Get( world->contacts, contactSim->contactId );
					if ( setIndex == b2_awakeSet )
					{
						// contact should be non-touching if awake
						// or it could be this contact hasn't been transferred yet
						B2_ASSERT( contactSim->manifold.pointCount == 0 ||
								   ( contactSim->simFlags & b2_simStartedTouching ) != 0 );
					}
					B2_ASSERT( contact->setIndex == setIndex );
					B2_ASSERT( contact->colorIndex == B2_NULL_INDEX );
					B2_ASSERT( contact->localIndex == i );

					b2Body* edgeBodyA = b2Array_Get( world->bodies, contact->edges[0].bodyId );
					b2Body* edgeBodyB = b2Array_Get( world->bodies, contact->edges[1].bodyId );
					B2_ASSERT( contactSim->encodedBodySimA == b2EncodeBodySimIndex( edgeBodyA ) );
					B2_ASSERT( contactSim->encodedBodySimB == b2EncodeBodySimIndex( edgeBodyB ) );
				}
			}

			// Validate joints
			{
				B2_ASSERT( set->jointSims.count >= 0 );
				totalJointCount += set->jointSims.count;
				for ( int i = 0; i < set->jointSims.count; ++i )
				{
					b2JointSim* jointSim = set->jointSims.data + i;
					b2Joint* joint = b2Array_Get( world->joints, jointSim->jointId );
					B2_ASSERT( joint->setIndex == setIndex );
					B2_ASSERT( joint->colorIndex == B2_NULL_INDEX );
					B2_ASSERT( joint->localIndex == i );
				}
			}

			// Validate islands
			{
				B2_ASSERT( set->islandSims.count >= 0 );
				totalIslandCount += set->islandSims.count;
				for ( int i = 0; i < set->islandSims.count; ++i )
				{
					b2IslandSim* islandSim = set->islandSims.data + i;
					b2Island* island = b2Array_Get( world->islands, islandSim->islandId );
					B2_ASSERT( island->setIndex == setIndex );
					B2_ASSERT( island->localIndex == i );
				}
			}
		}
		else
		{
			B2_ASSERT( set->bodySims.count == 0 );
			B2_ASSERT( set->contactSims.count == 0 );
			B2_ASSERT( set->jointSims.count == 0 );
			B2_ASSERT( set->islandSims.count == 0 );
			B2_ASSERT( set->bodyStates.count == 0 );
		}
	}

	int setIdCount = b2GetIdCount( &world->solverSetIdPool );
	B2_ASSERT( activeSetCount == setIdCount );

	int bodyIdCount = b2GetIdCount( &world->bodyIdPool );
	B2_ASSERT( totalBodyCount == bodyIdCount );

	int islandIdCount = b2GetIdCount( &world->islandIdPool );
	B2_ASSERT( totalIslandCount == islandIdCount );

	// Validate constraint graph
	for ( int colorIndex = 0; colorIndex < B2_GRAPH_COLOR_COUNT; ++colorIndex )
	{
		b2GraphColor* color = world->constraintGraph.colors + colorIndex;
		int bitCount = 0;

		B2_ASSERT( color->contactSims.count >= 0 );

		totalContactCount += color->contactSims.count;
		for ( int i = 0; i < color->contactSims.count; ++i )
		{
			b2ContactSim* contactSim = color->contactSims.data + i;
			b2Contact* contact = b2Array_Get( world->contacts, contactSim->contactId );
			// contact should be touching in the constraint graph or awaiting transfer to non-touching
			B2_ASSERT( contactSim->manifold.pointCount > 0 ||
					   ( contactSim->simFlags & ( b2_simStoppedTouching | b2_simDisjoint ) ) != 0 );
			B2_ASSERT( contact->setIndex == b2_awakeSet );
			B2_ASSERT( contact->colorIndex == colorIndex );
			B2_ASSERT( contact->localIndex == i );

			int bodyIdA = contact->edges[0].bodyId;
			int bodyIdB = contact->edges[1].bodyId;

			b2Body* edgeBodyA = b2Array_Get( world->bodies, bodyIdA );
			b2Body* edgeBodyB = b2Array_Get( world->bodies, bodyIdB );
			B2_ASSERT( contactSim->encodedBodySimA == b2EncodeBodySimIndex( edgeBodyA ) );
			B2_ASSERT( contactSim->encodedBodySimB == b2EncodeBodySimIndex( edgeBodyB ) );

			if ( colorIndex < B2_OVERFLOW_INDEX )
			{
				B2_ASSERT( b2GetBit( &color->bodySet, bodyIdA ) == ( edgeBodyA->type == b2_dynamicBody ) );
				B2_ASSERT( b2GetBit( &color->bodySet, bodyIdB ) == ( edgeBodyB->type == b2_dynamicBody ) );

				bitCount += edgeBodyA->type == b2_dynamicBody ? 1 : 0;
				bitCount += edgeBodyB->type == b2_dynamicBody ? 1 : 0;
			}
		}

		B2_ASSERT( color->jointSims.count >= 0 );
		totalJointCount += color->jointSims.count;
		for ( int i = 0; i < color->jointSims.count; ++i )
		{
			b2JointSim* jointSim = color->jointSims.data + i;
			b2Joint* joint = b2Array_Get( world->joints, jointSim->jointId );
			B2_ASSERT( joint->setIndex == b2_awakeSet );
			B2_ASSERT( joint->colorIndex == colorIndex );
			B2_ASSERT( joint->localIndex == i );

			int bodyIdA = joint->edges[0].bodyId;
			int bodyIdB = joint->edges[1].bodyId;

			if ( colorIndex < B2_OVERFLOW_INDEX )
			{
				b2Body* bodyA = b2Array_Get( world->bodies, bodyIdA );
				b2Body* bodyB = b2Array_Get( world->bodies, bodyIdB );
				B2_ASSERT( b2GetBit( &color->bodySet, bodyIdA ) == ( bodyA->type == b2_dynamicBody ) );
				B2_ASSERT( b2GetBit( &color->bodySet, bodyIdB ) == ( bodyB->type == b2_dynamicBody ) );

				bitCount += bodyA->type == b2_dynamicBody ? 1 : 0;
				bitCount += bodyB->type == b2_dynamicBody ? 1 : 0;
			}
		}

		// Validate the bit population for this graph color
		B2_ASSERT( bitCount == b2CountSetBits( &color->bodySet ) );
	}

	int contactIdCount = b2GetIdCount( &world->contactIdPool );
	B2_ASSERT( totalContactCount == contactIdCount );
	B2_ASSERT( totalContactCount == (int)world->broadPhase.pairSet.count );

	int jointIdCount = b2GetIdCount( &world->jointIdPool );
	B2_ASSERT( totalJointCount == jointIdCount );

// Validate shapes
// This is very slow on compounds
#if 0
	int shapeCapacity = world->shapes.count;
	for (int shapeIndex = 0; shapeIndex < shapeCapacity; shapeIndex += 1)
	{
		b2Shape* shape = world->shapes.data + shapeIndex;
		if (shape->id != shapeIndex)
		{
			continue;
		}

		b2Body* body = b2Array_Get(world->bodies, shape->bodyId);

		b2SolverSet* set = b2Array_Get(world->solverSets, body->setIndex);
		b2BodySim* bodySim = b2Array_Get(set->bodySims, body->localIndex);
		B2_ASSERT(bodySim->bodyId == shape->bodyId);

		bool found = false;
		int shapeCount = 0;
		int index = body->headShapeId;
		while (index != B2_NULL_INDEX)
		{
			b2Shape* s = b2Array_Get(world->shapes, index);
			if (index == shapeIndex)
			{
				found = true;
			}

			index = s->nextShapeId;
			shapeCount += 1;
		}

		B2_ASSERT(found);
		B2_ASSERT(shapeCount == body->shapeCount);
	}
#endif
}

// Validate contact touching status.
void b2ValidateContacts( b2World* world )
{
	int contactCount = world->contacts.count;
	B2_ASSERT( contactCount == b2GetIdCapacity( &world->contactIdPool ) );
	int allocatedContactCount = 0;

	for ( int contactIndex = 0; contactIndex < contactCount; ++contactIndex )
	{
		b2Contact* contact = b2Array_Get( world->contacts, contactIndex );
		if ( contact->contactId == B2_NULL_INDEX )
		{
			continue;
		}

		B2_ASSERT( contact->contactId == contactIndex );

		allocatedContactCount += 1;

		bool touching = ( contact->flags & b2_contactTouchingFlag ) != 0;

		int setId = contact->setIndex;

		if ( setId == b2_awakeSet )
		{
			if ( touching )
			{
				B2_ASSERT( 0 <= contact->colorIndex && contact->colorIndex < B2_GRAPH_COLOR_COUNT );
			}
			else
			{
				B2_ASSERT( contact->colorIndex == B2_NULL_INDEX );
			}
		}
		else if ( setId >= b2_firstSleepingSet )
		{
			// Only touching contacts allowed in a sleeping set
			B2_ASSERT( touching == true );
		}
		else
		{
			// Sleeping and non-touching contacts belong in the disabled set
			B2_ASSERT( touching == false && setId == b2_disabledSet );
		}

		b2ContactSim* contactSim = b2GetContactSim( world, contact );
		B2_ASSERT( contactSim->contactId == contactIndex );
		B2_ASSERT( contactSim->bodyIdA == contact->edges[0].bodyId );
		B2_ASSERT( contactSim->bodyIdB == contact->edges[1].bodyId );

		bool simTouching = ( contactSim->simFlags & b2_simTouchingFlag ) != 0;
		B2_ASSERT( touching == simTouching );

		B2_ASSERT( 0 <= contactSim->manifold.pointCount && contactSim->manifold.pointCount <= 2 );
	}

	int contactIdCount = b2GetIdCount( &world->contactIdPool );
	B2_ASSERT( allocatedContactCount == contactIdCount );
}

#else

void b2ValidateConnectivity( b2World* world )
{
	B2_UNUSED( world );
}

void b2ValidateSolverSets( b2World* world )
{
	B2_UNUSED( world );
}

void b2ValidateContacts( b2World* world )
{
	B2_UNUSED( world );
}

#endif