From 9314f303a7cdd6171f7ae06cd1bbf110efffe3cc Mon Sep 17 00:00:00 2001 From: Erin Catto Date: Wed, 28 Aug 2024 23:24:27 -0700 Subject: [PATCH] Cross platform determinism (#773) Cross platform determinism by implementing custom trigonometry functions 64-bit filter category and mask Fixed island bug Added `b2DefaultDebugDraw()` --- .github/workflows/build.yml | 5 +- CMakeLists.txt | 2 +- benchmark/main.c | 2 +- benchmark/tumbler.c | 8 +- docs/simulation.md | 9 +- include/box2d/base.h | 14 ++- include/box2d/box2d.h | 13 ++- include/box2d/collision.h | 22 ++-- include/box2d/math_functions.h | 122 +++++++++++++-------- include/box2d/types.h | 21 ++-- samples/CMakeLists.txt | 7 ++ samples/car.cpp | 4 +- samples/draw.cpp | 2 +- samples/sample_benchmark.cpp | 30 +++--- samples/sample_bodies.cpp | 12 +-- samples/sample_collision.cpp | 4 +- samples/sample_continuous.cpp | 4 +- samples/sample_determinism.cpp | 187 +++++++++++++++++++++++++++++++++ samples/sample_events.cpp | 12 +-- samples/sample_joints.cpp | 6 +- samples/sample_robustness.cpp | 4 +- samples/sample_shapes.cpp | 38 +++---- samples/sample_stacking.cpp | 4 +- samples/sample_world.cpp | 2 +- src/CMakeLists.txt | 13 +-- src/aabb.h | 7 -- src/body.c | 18 ++-- src/broad_phase.c | 6 +- src/broad_phase.h | 2 +- src/core.c | 2 +- src/core.h | 158 +++++++++++++--------------- src/distance_joint.c | 4 +- src/dynamic_tree.c | 12 +-- src/geometry.c | 4 +- src/island.c | 11 +- src/island.h | 3 +- src/joint.c | 13 ++- src/manifold.c | 4 +- src/math_functions.c | 106 ++++++++++++++----- src/revolute_joint.c | 13 ++- src/solver.c | 1 + src/timer.c | 25 +++-- src/types.c | 106 ++++++++++++++++++- src/world.c | 2 +- test/CMakeLists.txt | 5 +- test/test_determinism.c | 165 +++++++++++++++++++++++++++-- test/test_math.c | 26 +++++ test/test_world.c | 98 +++++++++++++++++ 48 files changed, 1012 insertions(+), 326 deletions(-) create mode 100644 samples/sample_determinism.cpp diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8f09562dd..818fb80f4 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -52,9 +52,8 @@ jobs: - uses: actions/checkout@v4 - name: Configure CMake - # some problem with simde - run: cmake -B ${{github.workspace}}/build -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} -DBOX2D_SAMPLES=OFF -DBOX2D_SANITIZE=ON -DBUILD_SHARED_LIBS=OFF - # run: cmake -B ${{github.workspace}}/build -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} -DBOX2D_SAMPLES=OFF -DBUILD_SHARED_LIBS=OFF + # run: cmake -B ${{github.workspace}}/build -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} -DBOX2D_SAMPLES=OFF -DBOX2D_SANITIZE=ON -DBUILD_SHARED_LIBS=OFF + run: cmake -B ${{github.workspace}}/build -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} -DBOX2D_SAMPLES=OFF -DBUILD_SHARED_LIBS=OFF - name: Build run: cmake --build ${{github.workspace}}/build --config ${{env.BUILD_TYPE}} diff --git a/CMakeLists.txt b/CMakeLists.txt index a909bab96..0c9648ca0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -3,7 +3,7 @@ include(FetchContent) include(CMakeDependentOption) project(box2d - VERSION 3.0.1 + VERSION 3.1.0 DESCRIPTION "A 2D physics engine for games" HOMEPAGE_URL "https://box2d.org" LANGUAGES C CXX diff --git a/benchmark/main.c b/benchmark/main.c index c026fbe96..eb280d8eb 100644 --- a/benchmark/main.c +++ b/benchmark/main.c @@ -183,7 +183,7 @@ int main( int argc, char** argv ) b2WorldDef worldDef = b2DefaultWorldDef(); worldDef.enableSleep = false; - worldDef.enableContinous = enableContinuous; + worldDef.enableContinuous = enableContinuous; worldDef.enqueueTask = EnqueueTask; worldDef.finishTask = FinishTask; worldDef.workerCount = threadCount; diff --git a/benchmark/tumbler.c b/benchmark/tumbler.c index fcfb77d3c..a501aa643 100644 --- a/benchmark/tumbler.c +++ b/benchmark/tumbler.c @@ -24,13 +24,13 @@ b2WorldId Tumbler( b2WorldDef* worldDef ) shapeDef.density = 50.0f; b2Polygon polygon; - polygon = b2MakeOffsetBox( 0.5f, 10.0f, ( b2Vec2 ){ 10.0f, 0.0f }, 0.0 ); + polygon = b2MakeOffsetBox( 0.5f, 10.0f, ( b2Vec2 ){ 10.0f, 0.0f }, b2Rot_identity ); b2CreatePolygonShape( bodyId, &shapeDef, &polygon ); - polygon = b2MakeOffsetBox( 0.5f, 10.0f, ( b2Vec2 ){ -10.0f, 0.0f }, 0.0 ); + polygon = b2MakeOffsetBox( 0.5f, 10.0f, ( b2Vec2 ){ -10.0f, 0.0f }, b2Rot_identity ); b2CreatePolygonShape( bodyId, &shapeDef, &polygon ); - polygon = b2MakeOffsetBox( 10.0f, 0.5f, ( b2Vec2 ){ 0.0f, 10.0f }, 0.0 ); + polygon = b2MakeOffsetBox( 10.0f, 0.5f, ( b2Vec2 ){ 0.0f, 10.0f }, b2Rot_identity ); b2CreatePolygonShape( bodyId, &shapeDef, &polygon ); - polygon = b2MakeOffsetBox( 10.0f, 0.5f, ( b2Vec2 ){ 0.0f, -10.0f }, 0.0 ); + polygon = b2MakeOffsetBox( 10.0f, 0.5f, ( b2Vec2 ){ 0.0f, -10.0f }, b2Rot_identity ); b2CreatePolygonShape( bodyId, &shapeDef, &polygon ); float motorSpeed = 25.0f; diff --git a/docs/simulation.md b/docs/simulation.md index 9f9502b78..a4f2bf2e8 100644 --- a/docs/simulation.md +++ b/docs/simulation.md @@ -455,8 +455,8 @@ scenarios that require altering the mass. b2MassData myMassData; myMassData.mass = 10.0f; myMassData.center = (b2Vec2){0.0f, 0.0f}; -myMassData.I = 100.0f; -b2Body_SetMassData(myBodyId, &myMassData); +myMassData.rotationalInertia = 100.0f; +b2Body_SetMassData(myBodyId, myMassData); ``` After setting a body's mass directly, you may wish to revert to the @@ -470,7 +470,7 @@ The body's mass data is available through the following functions: ```c float mass = b2Body_GetMass(myBodyId); -float inertia = b2Body_GetInertiaTensor(myBodyId); +float inertia = b2Body_GetRotationalInertia(myBodyId); b2Vec2 localCenter b2Body_GetLocalCenterOfMass(myBodyId); b2MassData massData = b2Body_GetMassData(myBodyId); ``` @@ -1448,8 +1448,7 @@ joint limit and a friction motor: ```c b2Vec2 worldPivot = {10.0f, -4.0f}; b2Vec2 worldAxis = {1.0f, 0.0f}; -b2PrismaticJointDef jointDef; -b2RevoluteJointDef jointDef = b2DefaultRevoluteJointDef(); +b2PrismaticJointDef jointDef = b2DefaultPrismaticJointDef(); jointDef.bodyIdA = myBodyIdA; jointDef.bodyIdB = myBodyIdB; jointDef.localAnchorA = b2Body_GetLocalPoint(myBodyIdA, worldPivot); diff --git a/include/box2d/base.h b/include/box2d/base.h index b98f21678..5d8a7deb3 100644 --- a/include/box2d/base.h +++ b/include/box2d/base.h @@ -5,23 +5,24 @@ #include +// clang-format off +// // Shared library macros #if defined( _MSC_VER ) && defined( box2d_EXPORTS ) // build the Windows DLL #define BOX2D_EXPORT __declspec( dllexport ) #elif defined( _MSC_VER ) && defined( BOX2D_DLL ) -// using the Windows DLL + // using the Windows DLL #define BOX2D_EXPORT __declspec( dllimport ) #elif defined( box2d_EXPORTS ) -// building or using the Box2D shared library + // building or using the Box2D shared library #define BOX2D_EXPORT __attribute__( ( visibility( "default" ) ) ) #else -// static library + // static library #define BOX2D_EXPORT #endif // C++ macros -// clang-format off #ifdef __cplusplus #define B2_API extern "C" BOX2D_EXPORT #define B2_INLINE inline @@ -104,4 +105,9 @@ B2_API float b2GetMilliseconds( const b2Timer* timer ); B2_API float b2GetMillisecondsAndReset( b2Timer* timer ); B2_API void b2SleepMilliseconds( int milliseconds ); B2_API void b2Yield( void ); + +// Simple djb2 hash function for determinism testing +#define B2_HASH_INIT 5381 +B2_API uint32_t b2Hash( uint32_t hash, const uint8_t* data, int count ); + //! @endcond diff --git a/include/box2d/box2d.h b/include/box2d/box2d.h index cfe8bc945..c7d80d5dc 100644 --- a/include/box2d/box2d.h +++ b/include/box2d/box2d.h @@ -288,8 +288,8 @@ B2_API void b2Body_ApplyAngularImpulse( b2BodyId bodyId, float impulse, bool wak /// Get the mass of the body, typically in kilograms B2_API float b2Body_GetMass( b2BodyId bodyId ); -/// Get the inertia tensor of the body, typically in kg*m^2 -B2_API float b2Body_GetInertiaTensor( b2BodyId bodyId ); +/// Get the rotational inertia of the body, typically in kg*m^2 +B2_API float b2Body_GetRotationalInertia( b2BodyId bodyId ); /// Get the center of mass position of the body in local space B2_API b2Vec2 b2Body_GetLocalCenterOfMass( b2BodyId bodyId ); @@ -353,7 +353,7 @@ B2_API void b2Body_EnableSleep( b2BodyId bodyId, bool enableSleep ); B2_API bool b2Body_IsSleepEnabled( b2BodyId bodyId ); /// Set the sleep threshold, typically in meters per second -B2_API void b2Body_SetSleepThreshold( b2BodyId bodyId, float sleepVelocity ); +B2_API void b2Body_SetSleepThreshold( b2BodyId bodyId, float sleepThreshold ); /// Get the sleep threshold, typically in meters per second. B2_API float b2Body_GetSleepThreshold( b2BodyId bodyId ); @@ -674,10 +674,10 @@ B2_API void b2DistanceJoint_SetSpringHertz( b2JointId jointId, float hertz ); B2_API void b2DistanceJoint_SetSpringDampingRatio( b2JointId jointId, float dampingRatio ); /// Get the spring Hertz -B2_API float b2DistanceJoint_GetHertz( b2JointId jointId ); +B2_API float b2DistanceJoint_GetSpringHertz( b2JointId jointId ); /// Get the spring damping ratio -B2_API float b2DistanceJoint_GetDampingRatio( b2JointId jointId ); +B2_API float b2DistanceJoint_GetSpringDampingRatio( b2JointId jointId ); /// Enable joint limit. The limit only works if the joint spring is enabled. Otherwise the joint is rigid /// and the limit has no effect. @@ -893,6 +893,9 @@ B2_API b2JointId b2CreateRevoluteJoint( b2WorldId worldId, const b2RevoluteJoint /// Enable/disable the revolute joint spring B2_API void b2RevoluteJoint_EnableSpring( b2JointId jointId, bool enableSpring ); +/// It the revolute angular spring enabled? +B2_API bool b2RevoluteJoint_IsSpringEnabled( b2JointId jointId ); + /// Set the revolute joint spring stiffness in Hertz B2_API void b2RevoluteJoint_SetSpringHertz( b2JointId jointId, float hertz ); diff --git a/include/box2d/collision.h b/include/box2d/collision.h index 61c7d12fb..1430e3201 100644 --- a/include/box2d/collision.h +++ b/include/box2d/collision.h @@ -192,7 +192,7 @@ B2_API b2Polygon b2MakeBox( float hx, float hy ); B2_API b2Polygon b2MakeRoundedBox( float hx, float hy, float radius ); /// Make an offset box, bypassing the need for a convex hull. -B2_API b2Polygon b2MakeOffsetBox( float hx, float hy, b2Vec2 center, float angle ); +B2_API b2Polygon b2MakeOffsetBox( float hx, float hy, b2Vec2 center, b2Rot rotation ); /// Transform a polygon. This is useful for transferring a shape from one body to another. B2_API b2Polygon b2TransformPolygon( b2Transform transform, const b2Polygon* polygon ); @@ -589,21 +589,20 @@ B2_API b2Manifold b2CollideSmoothSegmentAndPolygon( const b2SmoothSegment* smoot */ /// The default category bit for a tree proxy. Used for collision filtering. -#define b2_defaultCategoryBits ( 0x00000001 ) +#define b2_defaultCategoryBits ( 1 ) /// Convenience mask bits to use when you don't need collision filtering and just want /// all results. -#define b2_defaultMaskBits ( 0xFFFFFFFF ) +#define b2_defaultMaskBits ( UINT64_MAX ) /// A node in the dynamic tree. This is private data placed here for performance reasons. -/// 16 + 16 + 8 + pad(8) typedef struct b2TreeNode { /// The node bounding box b2AABB aabb; // 16 /// Category bits for collision filtering - uint32_t categoryBits; // 4 + uint64_t categoryBits; // 8 union { @@ -631,7 +630,7 @@ typedef struct b2TreeNode bool enlarged; // 1 /// Padding for clarity - char pad[9]; + char pad[5]; } b2TreeNode; /// The dynamic tree structure. This should be considered private data. @@ -679,7 +678,7 @@ B2_API b2DynamicTree b2DynamicTree_Create( void ); B2_API void b2DynamicTree_Destroy( b2DynamicTree* tree ); /// Create a proxy. Provide an AABB and a userData value. -B2_API int32_t b2DynamicTree_CreateProxy( b2DynamicTree* tree, b2AABB aabb, uint32_t categoryBits, int32_t userData ); +B2_API int32_t b2DynamicTree_CreateProxy( b2DynamicTree* tree, b2AABB aabb, uint64_t categoryBits, int32_t userData ); /// Destroy a proxy. This asserts if the id is invalid. B2_API void b2DynamicTree_DestroyProxy( b2DynamicTree* tree, int32_t proxyId ); @@ -694,9 +693,8 @@ B2_API void b2DynamicTree_EnlargeProxy( b2DynamicTree* tree, int32_t proxyId, b2 /// @return true if the query should continue typedef bool b2TreeQueryCallbackFcn( int32_t proxyId, int32_t userData, void* context ); -/// Query an AABB for overlapping proxies. The callback class -/// is called for each proxy that overlaps the supplied AABB. -B2_API void b2DynamicTree_Query( const b2DynamicTree* tree, b2AABB aabb, uint32_t maskBits, b2TreeQueryCallbackFcn* callback, +/// Query an AABB for overlapping proxies. The callback class is called for each proxy that overlaps the supplied AABB. +B2_API void b2DynamicTree_Query( const b2DynamicTree* tree, b2AABB aabb, uint64_t maskBits, b2TreeQueryCallbackFcn* callback, void* context ); /// This function receives clipped raycast input for a proxy. The function @@ -717,7 +715,7 @@ typedef float b2TreeRayCastCallbackFcn( const b2RayCastInput* input, int32_t pro /// @param maskBits filter bits: `bool accept = (maskBits & node->categoryBits) != 0;` /// @param callback a callback class that is called for each proxy that is hit by the ray /// @param context user context that is passed to the callback -B2_API void b2DynamicTree_RayCast( const b2DynamicTree* tree, const b2RayCastInput* input, uint32_t maskBits, +B2_API void b2DynamicTree_RayCast( const b2DynamicTree* tree, const b2RayCastInput* input, uint64_t maskBits, b2TreeRayCastCallbackFcn* callback, void* context ); /// This function receives clipped ray-cast input for a proxy. The function @@ -737,7 +735,7 @@ typedef float b2TreeShapeCastCallbackFcn( const b2ShapeCastInput* input, int32_t /// @param maskBits filter bits: `bool accept = (maskBits & node->categoryBits) != 0;` /// @param callback a callback class that is called for each proxy that is hit by the shape /// @param context user context that is passed to the callback -B2_API void b2DynamicTree_ShapeCast( const b2DynamicTree* tree, const b2ShapeCastInput* input, uint32_t maskBits, +B2_API void b2DynamicTree_ShapeCast( const b2DynamicTree* tree, const b2ShapeCastInput* input, uint64_t maskBits, b2TreeShapeCastCallbackFcn* callback, void* context ); /// Validate this tree. For testing. diff --git a/include/box2d/math_functions.h b/include/box2d/math_functions.h index 31955f3e1..fb29ba984 100644 --- a/include/box2d/math_functions.h +++ b/include/box2d/math_functions.h @@ -5,6 +5,7 @@ #include "base.h" +#include #include #include @@ -66,6 +67,11 @@ static const b2Rot b2Rot_identity = { 1.0f, 0.0f }; static const b2Transform b2Transform_identity = { { 0.0f, 0.0f }, { 1.0f, 0.0f } }; static const b2Mat22 b2Mat22_zero = { { 0.0f, 0.0f }, { 0.0f, 0.0f } }; +/// Compute an approximate arctangent in the range [-pi, pi] +/// This is hand coded for cross platform determinism. The atan2f +/// function in the standard library is not cross platform deterministic. +B2_API float b2Atan2( float y, float x ); + /// @return the minimum of two floats B2_INLINE float b2MinFloat( float a, float b ) { @@ -241,12 +247,6 @@ B2_INLINE float b2Length( b2Vec2 v ) return sqrtf( v.x * v.x + v.y * v.y ); } -/// Get the length squared of this vector -B2_INLINE float b2LengthSquared( b2Vec2 v ) -{ - return v.x * v.x + v.y * v.y; -} - /// Get the distance between two points B2_INLINE float b2Distance( b2Vec2 a, b2Vec2 b ) { @@ -255,19 +255,33 @@ B2_INLINE float b2Distance( b2Vec2 a, b2Vec2 b ) return sqrtf( dx * dx + dy * dy ); } -/// Get the distance squared between points -B2_INLINE float b2DistanceSquared( b2Vec2 a, b2Vec2 b ) +/// Convert a vector into a unit vector if possible, otherwise returns the zero vector. +B2_INLINE b2Vec2 b2Normalize( b2Vec2 v ) { - b2Vec2 c = { b.x - a.x, b.y - a.y }; - return c.x * c.x + c.y * c.y; + float length = sqrtf( v.x * v.x + v.y * v.y ); + if ( length < FLT_EPSILON ) + { + return b2Vec2_zero; + } + + float invLength = 1.0f / length; + b2Vec2 n = { invLength * v.x, invLength * v.y }; + return n; } -/// Make a rotation using an angle in radians -B2_INLINE b2Rot b2MakeRot( float angle ) +/// Convert a vector into a unit vector if possible, otherwise returns the zero vector. Also +/// outputs the length. +B2_INLINE b2Vec2 b2GetLengthAndNormalize( float* length, b2Vec2 v ) { - // todo determinism - b2Rot q = { cosf( angle ), sinf( angle ) }; - return q; + *length = b2Length( v ); + if ( *length < FLT_EPSILON ) + { + return b2Vec2_zero; + } + + float invLength = 1.0f / *length; + b2Vec2 n = { invLength * v.x, invLength * v.y }; + return n; } /// Normalize rotation @@ -279,6 +293,38 @@ B2_INLINE b2Rot b2NormalizeRot( b2Rot q ) return qn; } +/// Integration rotation from angular velocity +/// @param q1 initial rotation +/// @param deltaAngle the angular displacement in radians +B2_INLINE b2Rot b2IntegrateRotation( b2Rot q1, float deltaAngle ) +{ + // dc/dt = -omega * sin(t) + // ds/dt = omega * cos(t) + // c2 = c1 - omega * h * s1 + // s2 = s1 + omega * h * c1 + b2Rot q2 = { q1.c - deltaAngle * q1.s, q1.s + deltaAngle * q1.c }; + float mag = sqrtf( q2.s * q2.s + q2.c * q2.c ); + float invMag = mag > 0.0 ? 1.0f / mag : 0.0f; + b2Rot qn = { q2.c * invMag, q2.s * invMag }; + return qn; +} + +/// Get the length squared of this vector +B2_INLINE float b2LengthSquared( b2Vec2 v ) +{ + return v.x * v.x + v.y * v.y; +} + +/// Get the distance squared between points +B2_INLINE float b2DistanceSquared( b2Vec2 a, b2Vec2 b ) +{ + b2Vec2 c = { b.x - a.x, b.y - a.y }; + return c.x * c.x + c.y * c.y; +} + +/// Make a rotation using an angle in radians +B2_API b2Rot b2MakeRot( float angle ); + /// Is this rotation normalized? B2_INLINE bool b2IsNormalized( b2Rot q ) { @@ -300,21 +346,6 @@ B2_INLINE b2Rot b2NLerp( b2Rot q1, b2Rot q2, float t ) return b2NormalizeRot( q ); } -/// Integration rotation from angular velocity -/// @param q1 initial rotation -/// @param deltaAngle the angular displacement in radians -B2_INLINE b2Rot b2IntegrateRotation( b2Rot q1, float deltaAngle ) -{ - // dc/dt = -omega * sin(t) - // ds/dt = omega * cos(t) - // c2 = c1 - omega * h * s1 - // s2 = s1 + omega * h * c1 - b2Rot q2 = { q1.c - deltaAngle * q1.s, q1.s + deltaAngle * q1.c }; - float mag = sqrtf( q2.s * q2.s + q2.c * q2.c ); - float invMag = mag > 0.0 ? 1.0f / mag : 0.0f; - b2Rot qn = { q2.c * invMag, q2.s * invMag }; - return qn; -} /// Compute the angular velocity necessary to rotate between two rotations over a give time /// @param q1 initial rotation @@ -339,8 +370,7 @@ B2_INLINE float b2ComputeAngularVelocity( b2Rot q1, b2Rot q2, float inv_h ) /// Get the angle in radians in the range [-pi, pi] B2_INLINE float b2Rot_GetAngle( b2Rot q ) { - // todo determinism - return atan2f( q.s, q.c ); + return b2Atan2( q.s, q.c ); } /// Get the x-axis @@ -390,7 +420,7 @@ B2_INLINE float b2RelativeAngle( b2Rot b, b2Rot a ) // cos(b - a) = bc * ac + bs * as float s = b.s * a.c - b.c * a.s; float c = b.c * a.c + b.s * a.s; - return atan2f( s, c ); + return b2Atan2( s, c ); } /// Convert an angle in the range [-2*pi, 2*pi] into the range [-pi, pi] @@ -408,6 +438,22 @@ B2_INLINE float b2UnwindAngle( float angle ) return angle; } +/// Convert any into the range [-pi, pi] (slow) +B2_INLINE float b2UnwindLargeAngle( float angle ) +{ + while ( angle > b2_pi ) + { + angle -= 2.0f * b2_pi; + } + + while ( angle < -b2_pi ) + { + angle += 2.0f * b2_pi; + } + + return angle; +} + /// Rotate a vector B2_INLINE b2Vec2 b2RotateVector( b2Rot q, b2Vec2 v ) { @@ -546,16 +592,6 @@ B2_API bool b2Rot_IsValid( b2Rot q ); /// Is this a valid bounding box? Not Nan or infinity. Upper bound greater than or equal to lower bound. B2_API bool b2AABB_IsValid( b2AABB aabb ); -/// Convert a vector into a unit vector if possible, otherwise returns the zero vector. -B2_API b2Vec2 b2Normalize( b2Vec2 v ); - -/// Convert a vector into a unit vector if possible, otherwise asserts. -B2_API b2Vec2 b2NormalizeChecked( b2Vec2 v ); - -/// Convert a vector into a unit vector if possible, otherwise returns the zero vector. Also -/// outputs the length. -B2_API b2Vec2 b2GetLengthAndNormalize( float* length, b2Vec2 v ); - /// Box2D bases all length units on meters, but you may need different units for your game. /// You can set this value to use different units. This should be done at application startup /// and only modified once. Default value is 1. diff --git a/include/box2d/types.h b/include/box2d/types.h index 54f395ed5..d6c753b36 100644 --- a/include/box2d/types.h +++ b/include/box2d/types.h @@ -95,7 +95,7 @@ typedef struct b2WorldDef bool enableSleep; /// Enable continuous collision - bool enableContinous; + bool enableContinuous; /// Number of workers to use with the provided task system. Box2D performs best when using only /// performance cores and accessing a single L2 cache. Efficiency cores and hyper-threading provide @@ -234,7 +234,7 @@ typedef struct b2Filter /// // etc /// }; /// @endcode - uint32_t categoryBits; + uint64_t categoryBits; /// The collision mask bits. This states the categories that this /// shape would accept for collision. @@ -243,7 +243,7 @@ typedef struct b2Filter /// @code{.c} /// maskBits = Static | Player; /// @endcode - uint32_t maskBits; + uint64_t maskBits; /// Collision groups allow a certain group of objects to never collide (negative) /// or always collide (positive). A group index of zero has no effect. Non-zero group filtering @@ -265,11 +265,11 @@ B2_API b2Filter b2DefaultFilter( void ); typedef struct b2QueryFilter { /// The collision category bits of this query. Normally you would just set one bit. - uint32_t categoryBits; + uint64_t categoryBits; /// The collision mask bits. This states the shape categories that this /// query would accept for collision. - uint32_t maskBits; + uint64_t maskBits; } b2QueryFilter; /// Use this to initialize your query filter @@ -325,6 +325,8 @@ typedef struct b2ShapeDef uint32_t customColor; /// A sensor shape generates overlap events but never generates a collision response. + /// Sensors do not collide with other sensors and do not have continuous collision. + /// Instead use a ray or shape cast for those scenarios. bool isSensor; /// Enable sensor events for this shape. Only applies to kinematic and dynamic bodies. Ignored for sensors. @@ -1082,7 +1084,6 @@ typedef enum b2HexColor { b2_colorAliceBlue = 0xf0f8ff, b2_colorAntiqueWhite = 0xfaebd7, - b2_colorAqua = 0x00ffff, b2_colorAquamarine = 0x7fffd4, b2_colorAzure = 0xf0ffff, b2_colorBeige = 0xf5f5dc, @@ -1125,7 +1126,6 @@ typedef enum b2HexColor b2_colorFirebrick = 0xb22222, b2_colorFloralWhite = 0xfffaf0, b2_colorForestGreen = 0x228b22, - b2_colorFuchsia = 0xff00ff, b2_colorGainsboro = 0xdcdcdc, b2_colorGhostWhite = 0xf8f8ff, b2_colorGold = 0xffd700, @@ -1167,7 +1167,6 @@ typedef enum b2HexColor b2_colorLightSlateGray = 0x778899, b2_colorLightSteelBlue = 0xb0c4de, b2_colorLightYellow = 0xffffe0, - b2_colorLime = 0x00ff00, b2_colorLimeGreen = 0x32cd32, b2_colorLinen = 0xfaf0e6, b2_colorMagenta = 0xff00ff, @@ -1186,7 +1185,6 @@ typedef enum b2HexColor b2_colorMistyRose = 0xffe4e1, b2_colorMoccasin = 0xffe4b5, b2_colorNavajoWhite = 0xffdead, - b2_colorNavy = 0x000080, b2_colorNavyBlue = 0x000080, b2_colorOldLace = 0xfdf5e6, b2_colorOlive = 0x808000, @@ -1241,6 +1239,7 @@ typedef enum b2HexColor } b2HexColor; /// This struct holds callbacks you can implement to draw a Box2D world. +/// This structure should be zero initialized. /// @ingroup world typedef struct b2DebugDraw { @@ -1314,3 +1313,7 @@ typedef struct b2DebugDraw /// User context that is passed as an argument to drawing callback functions void* context; } b2DebugDraw; + +/// Use this to initialize your drawing interface. This allows you to implement a sub-set +/// of the drawing functions. +B2_API b2DebugDraw b2DefaultDebugDraw( void ); diff --git a/samples/CMakeLists.txt b/samples/CMakeLists.txt index dd136f7ce..477afa08d 100644 --- a/samples/CMakeLists.txt +++ b/samples/CMakeLists.txt @@ -73,6 +73,7 @@ add_executable(samples sample_bodies.cpp sample_collision.cpp sample_continuous.cpp + sample_determinism.cpp sample_events.cpp sample_geometry.cpp sample_joints.cpp @@ -92,6 +93,12 @@ set_target_properties(samples PROPERTIES CXX_EXTENSIONS NO ) + +if (APPLE) + # determinism settings + target_compile_options(samples PRIVATE -ffp-model=precise -ffp-contract=off) +endif() + target_include_directories(samples PRIVATE ${CMAKE_CURRENT_SOURCE_DIR} ${JSMN_DIR}) target_link_libraries(samples PUBLIC box2d imgui glfw glad enkiTS) diff --git a/samples/car.cpp b/samples/car.cpp index 30bf115db..e54679e0c 100644 --- a/samples/car.cpp +++ b/samples/car.cpp @@ -192,11 +192,11 @@ void Truck::Spawn( b2WorldId worldId, b2Vec2 position, float scale, float hertz, m_chassisId = b2CreateBody( worldId, &bodyDef ); b2CreatePolygonShape( m_chassisId, &shapeDef, &chassis ); - b2Polygon box = b2MakeOffsetBox( 1.25f * scale, 0.1f * scale, { -2.05f * scale, -0.275f * scale }, 0.0f ); + b2Polygon box = b2MakeOffsetBox( 1.25f * scale, 0.1f * scale, { -2.05f * scale, -0.275f * scale }, b2Rot_identity ); box.radius = 0.1f * scale; b2CreatePolygonShape( m_chassisId, &shapeDef, &box ); - box = b2MakeOffsetBox( 0.05f * scale, 0.35f * scale, { -3.25f * scale, 0.375f * scale }, 0.0f ); + box = b2MakeOffsetBox( 0.05f * scale, 0.35f * scale, { -3.25f * scale, 0.375f * scale }, b2Rot_identity ); box.radius = 0.1f * scale; b2CreatePolygonShape( m_chassisId, &shapeDef, &box ); diff --git a/samples/draw.cpp b/samples/draw.cpp index b134b8ad2..2c46702e5 100644 --- a/samples/draw.cpp +++ b/samples/draw.cpp @@ -228,7 +228,7 @@ struct GLPoints "{\n" " f_color = v_color;\n" " gl_Position = projectionMatrix * vec4(v_position, 0.0f, 1.0f);\n" - " gl_PointSize = v_size;\n" + " gl_PointSize = v_size;\n" "}\n"; const char* fs = "#version 330\n" diff --git a/samples/sample_benchmark.cpp b/samples/sample_benchmark.cpp index 8878a0ad1..d684d68ce 100644 --- a/samples/sample_benchmark.cpp +++ b/samples/sample_benchmark.cpp @@ -52,13 +52,13 @@ class BenchmarkBarrel : public Sample b2ShapeDef shapeDef = b2DefaultShapeDef(); b2CreatePolygonShape( groundId, &shapeDef, &box ); - box = b2MakeOffsetBox( 1.2f, 2.0f * groundSize, { -groundSize, 2.0f * groundSize }, 0.0f ); + box = b2MakeOffsetBox( 1.2f, 2.0f * groundSize, { -groundSize, 2.0f * groundSize }, b2Rot_identity ); b2CreatePolygonShape( groundId, &shapeDef, &box ); - box = b2MakeOffsetBox( 1.2f, 2.0f * groundSize, { groundSize, 2.0f * groundSize }, 0.0f ); + box = b2MakeOffsetBox( 1.2f, 2.0f * groundSize, { groundSize, 2.0f * groundSize }, b2Rot_identity ); b2CreatePolygonShape( groundId, &shapeDef, &box ); - box = b2MakeOffsetBox( 800.0f, 10.0f, { 0.0f, -80.0f }, 0.0f ); + box = b2MakeOffsetBox( 800.0f, 10.0f, { 0.0f, -80.0f }, b2Rot_identity ); b2CreatePolygonShape( groundId, &shapeDef, &box ); } @@ -320,13 +320,13 @@ class BenchmarkTumbler : public Sample shapeDef.density = 50.0f; b2Polygon polygon; - polygon = b2MakeOffsetBox( 0.5f, 10.0f, { 10.0f, 0.0f }, 0.0 ); + polygon = b2MakeOffsetBox( 0.5f, 10.0f, { 10.0f, 0.0f }, b2Rot_identity ); b2CreatePolygonShape( bodyId, &shapeDef, &polygon ); - polygon = b2MakeOffsetBox( 0.5f, 10.0f, { -10.0f, 0.0f }, 0.0 ); + polygon = b2MakeOffsetBox( 0.5f, 10.0f, { -10.0f, 0.0f }, b2Rot_identity ); b2CreatePolygonShape( bodyId, &shapeDef, &polygon ); - polygon = b2MakeOffsetBox( 10.0f, 0.5f, { 0.0f, 10.0f }, 0.0 ); + polygon = b2MakeOffsetBox( 10.0f, 0.5f, { 0.0f, 10.0f }, b2Rot_identity ); b2CreatePolygonShape( bodyId, &shapeDef, &polygon ); - polygon = b2MakeOffsetBox( 10.0f, 0.5f, { 0.0f, -10.0f }, 0.0 ); + polygon = b2MakeOffsetBox( 10.0f, 0.5f, { 0.0f, -10.0f }, b2Rot_identity ); b2CreatePolygonShape( bodyId, &shapeDef, &polygon ); // m_motorSpeed = 9.0f; @@ -456,13 +456,13 @@ class BenchmarkManyTumblers : public Sample shapeDef.density = 50.0f; b2Polygon polygon; - polygon = b2MakeOffsetBox( 0.25f, 2.0f, { 2.0f, 0.0f }, 0.0 ); + polygon = b2MakeOffsetBox( 0.25f, 2.0f, { 2.0f, 0.0f }, b2Rot_identity ); b2CreatePolygonShape( bodyId, &shapeDef, &polygon ); - polygon = b2MakeOffsetBox( 0.25f, 2.0f, { -2.0f, 0.0f }, 0.0 ); + polygon = b2MakeOffsetBox( 0.25f, 2.0f, { -2.0f, 0.0f }, b2Rot_identity ); b2CreatePolygonShape( bodyId, &shapeDef, &polygon ); - polygon = b2MakeOffsetBox( 2.0f, 0.25f, { 0.0f, 2.0f }, 0.0 ); + polygon = b2MakeOffsetBox( 2.0f, 0.25f, { 0.0f, 2.0f }, b2Rot_identity ); b2CreatePolygonShape( bodyId, &shapeDef, &polygon ); - polygon = b2MakeOffsetBox( 2.0f, 0.25f, { 0.0f, -2.0f }, 0.0 ); + polygon = b2MakeOffsetBox( 2.0f, 0.25f, { 0.0f, -2.0f }, b2Rot_identity ); b2CreatePolygonShape( bodyId, &shapeDef, &polygon ); } @@ -1407,7 +1407,7 @@ class BenchmarkCompound : public Sample for ( int j = i; j < width; ++j ) { float x = grid * j; - b2Polygon square = b2MakeOffsetBox( 0.5f * grid, 0.5f * grid, { x, y }, 0.0f ); + b2Polygon square = b2MakeOffsetBox( 0.5f * grid, 0.5f * grid, { x, y }, b2Rot_identity ); b2CreatePolygonShape( groundId, &shapeDef, &square ); } } @@ -1418,7 +1418,7 @@ class BenchmarkCompound : public Sample for ( int j = i; j < width; ++j ) { float x = -grid * j; - b2Polygon square = b2MakeOffsetBox( 0.5f * grid, 0.5f * grid, { x, y }, 0.0f ); + b2Polygon square = b2MakeOffsetBox( 0.5f * grid, 0.5f * grid, { x, y }, b2Rot_identity ); b2CreatePolygonShape( groundId, &shapeDef, &square ); } } @@ -1455,7 +1455,7 @@ class BenchmarkCompound : public Sample for ( int j = 0; j < span; ++j ) { float x = j * grid; - b2Polygon square = b2MakeOffsetBox( 0.5f * grid, 0.5f * grid, { x, y }, 0.0f ); + b2Polygon square = b2MakeOffsetBox( 0.5f * grid, 0.5f * grid, { x, y }, b2Rot_identity ); b2CreatePolygonShape( bodyId, &shapeDef, &square ); } } @@ -1513,7 +1513,7 @@ class BenchmarkKinematic : public Sample for ( int j = -span; j < span; ++j ) { float x = j * grid; - b2Polygon square = b2MakeOffsetBox( 0.5f * grid, 0.5f * grid, { x, y }, 0.0f ); + b2Polygon square = b2MakeOffsetBox( 0.5f * grid, 0.5f * grid, { x, y }, b2Rot_identity ); b2CreatePolygonShape( bodyId, &shapeDef, &square ); } } diff --git a/samples/sample_bodies.cpp b/samples/sample_bodies.cpp index a08d368c8..a9ef0d550 100644 --- a/samples/sample_bodies.cpp +++ b/samples/sample_bodies.cpp @@ -69,7 +69,7 @@ class BodyType : public Sample bodyDef.position = { -4.0f, 5.0f }; m_platformId = b2CreateBody( m_worldId, &bodyDef ); - b2Polygon box = b2MakeOffsetBox( 0.5f, 4.0f, { 4.0f, 0.0f }, 0.5f * b2_pi ); + b2Polygon box = b2MakeOffsetBox( 0.5f, 4.0f, { 4.0f, 0.0f }, b2MakeRot(0.5f * b2_pi) ); b2ShapeDef shapeDef = b2DefaultShapeDef(); shapeDef.friction = 0.6f; @@ -353,13 +353,13 @@ class Character : public Sample b2BodyId groundId = b2CreateBody( m_worldId, &bodyDef ); b2ShapeDef shapeDef = b2DefaultShapeDef(); - b2Polygon box = b2MakeOffsetBox( 1.0f, 1.0f, { 4.0f, 3.0f }, 0.0f ); + b2Polygon box = b2MakeOffsetBox( 1.0f, 1.0f, { 4.0f, 3.0f }, b2Rot_identity ); b2CreatePolygonShape( groundId, &shapeDef, &box ); - box = b2MakeOffsetBox( 1.0f, 1.0f, { 6.0f, 3.0f }, 0.0f ); + box = b2MakeOffsetBox( 1.0f, 1.0f, { 6.0f, 3.0f }, b2Rot_identity ); b2CreatePolygonShape( groundId, &shapeDef, &box ); - box = b2MakeOffsetBox( 1.0f, 1.0f, { 8.0f, 3.0f }, 0.0f ); + box = b2MakeOffsetBox( 1.0f, 1.0f, { 8.0f, 3.0f }, b2Rot_identity ); b2CreatePolygonShape( groundId, &shapeDef, &box ); } @@ -506,7 +506,7 @@ class Weeble : public Sample b2CreateCapsuleShape( m_weebleId, &shapeDef, &capsule ); float mass = b2Body_GetMass( m_weebleId ); - float inertiaTensor = b2Body_GetInertiaTensor( m_weebleId ); + float inertiaTensor = b2Body_GetRotationalInertia( m_weebleId ); float offset = 1.5f; @@ -630,7 +630,7 @@ class Sleep : public Sample bodyDef.enableSleep = false; b2BodyId bodyId = b2CreateBody( m_worldId, &bodyDef ); - b2Polygon box = b2MakeOffsetBox( 1.0f, 1.0f, { 0.0f, 1.0f }, 0.25f * b2_pi ); + b2Polygon box = b2MakeOffsetBox( 1.0f, 1.0f, { 0.0f, 1.0f }, b2MakeRot(0.25f * b2_pi) ); b2ShapeDef shapeDef = b2DefaultShapeDef(); b2CreatePolygonShape( bodyId, &shapeDef, &box ); } diff --git a/samples/sample_collision.cpp b/samples/sample_collision.cpp index ff07e5282..869727824 100644 --- a/samples/sample_collision.cpp +++ b/samples/sample_collision.cpp @@ -320,7 +320,7 @@ class ShapeDistance : public Sample m_simplexCount = output.simplexCount; - DrawShape( m_typeA, b2Transform_identity, m_radiusA, b2_colorAqua ); + DrawShape( m_typeA, b2Transform_identity, m_radiusA, b2_colorCyan ); DrawShape( m_typeB, m_transform, m_radiusB, b2_colorBisque ); if ( m_drawSimplex ) @@ -2485,7 +2485,7 @@ class Manifold : public Sample // box-capsule { b2Capsule capsule = { { -0.4f, 0.0f }, { -0.1f, 0.0f }, 0.1f }; - b2Polygon box = b2MakeOffsetBox( 0.25f, 1.0f, { 1.0f, -1.0f }, 0.25f * b2_pi ); + b2Polygon box = b2MakeOffsetBox( 0.25f, 1.0f, { 1.0f, -1.0f }, b2MakeRot(0.25f * b2_pi) ); b2Transform transform1 = { offset, b2Rot_identity }; b2Transform transform2 = { b2Add( m_transform.p, offset ), m_transform.q }; diff --git a/samples/sample_continuous.cpp b/samples/sample_continuous.cpp index b1984bfe8..a8dbe85fa 100644 --- a/samples/sample_continuous.cpp +++ b/samples/sample_continuous.cpp @@ -281,7 +281,7 @@ class SkinnyBox : public Sample shapeDef.friction = 0.9f; b2CreateSegmentShape( groundId, &shapeDef, &segment ); - b2Polygon box = b2MakeOffsetBox( 0.1f, 1.0f, { 0.0f, 1.0f }, 0.0f ); + b2Polygon box = b2MakeOffsetBox( 0.1f, 1.0f, { 0.0f, 1.0f }, b2Rot_identity ); b2CreatePolygonShape( groundId, &shapeDef, &box ); } @@ -717,7 +717,7 @@ class SpeculativeFallback : public Sample b2BodyId bodyId = b2CreateBody( m_worldId, &bodyDef ); b2ShapeDef shapeDef = b2DefaultShapeDef(); - b2Polygon box = b2MakeOffsetBox( 2.0f, 0.05f, { -offset, 0.0f }, b2_pi ); + b2Polygon box = b2MakeOffsetBox( 2.0f, 0.05f, { -offset, 0.0f }, b2MakeRot(b2_pi) ); b2CreatePolygonShape( bodyId, &shapeDef, &box ); } } diff --git a/samples/sample_determinism.cpp b/samples/sample_determinism.cpp new file mode 100644 index 000000000..d92de3d19 --- /dev/null +++ b/samples/sample_determinism.cpp @@ -0,0 +1,187 @@ +// SPDX-FileCopyrightText: 2022 Erin Catto +// SPDX-License-Identifier: MIT + +#include "draw.h" +#include "sample.h" +#include "settings.h" + +#include "box2d/box2d.h" +#include "box2d/math_functions.h" + +#include +#include + +// This sample provides a visual representation of the cross platform determinism unit test. +// The scenario is designed to produce a chaotic result engaging: +// - continuous collision +// - joint limits (approximate atan2) +// - b2MakeRot (approximate sin/cos) +// Once all the bodies go to sleep the step counter and transform hash is emitted which +// can then be transferred to the unit test and tested in GitHub build actions. +// See CrossPlatformTest in the unit tests. +class FallingHinges : public Sample +{ +public: + enum + { + e_columns = 4, + e_rows = 30, + }; + + explicit FallingHinges( Settings& settings ) + : Sample( settings ) + { + if ( settings.restart == false ) + { + g_camera.m_center = { 0.0f, 7.5f }; + g_camera.m_zoom = 10.0f; + } + + { + b2BodyDef bodyDef = b2DefaultBodyDef(); + bodyDef.position = { 0.0f, -1.0f }; + b2BodyId groundId = b2CreateBody( m_worldId, &bodyDef ); + + b2Polygon box = b2MakeBox( 20.0f, 1.0f ); + b2ShapeDef shapeDef = b2DefaultShapeDef(); + b2CreatePolygonShape( groundId, &shapeDef, &box ); + } + + for ( int i = 0; i < e_rows * e_columns; ++i ) + { + m_bodies[i] = b2_nullBodyId; + } + + float h = 0.25f; + float r = 0.1f * h; + b2Polygon box = b2MakeRoundedBox( h - r, h - r, r ); + + b2ShapeDef shapeDef = b2DefaultShapeDef(); + shapeDef.friction = 0.3f; + + float offset = 0.4f * h; + float dx = 10.0f * h; + float xroot = -0.5f * dx * ( e_columns - 1.0f ); + + b2RevoluteJointDef jointDef = b2DefaultRevoluteJointDef(); + jointDef.enableLimit = true; + jointDef.lowerAngle = -0.1f * b2_pi; + jointDef.upperAngle = 0.2f * b2_pi; + jointDef.enableSpring = true; + jointDef.hertz = 0.5f; + jointDef.dampingRatio = 0.5f; + jointDef.localAnchorA = { h, h }; + jointDef.localAnchorB = { offset, -h }; + jointDef.drawSize = 0.1f; + + int bodyIndex = 0; + int bodyCount = e_rows * e_columns; + + for ( int j = 0; j < e_columns; ++j ) + { + float x = xroot + j * dx; + + b2BodyId prevBodyId = b2_nullBodyId; + + for ( int i = 0; i < e_rows; ++i ) + { + b2BodyDef bodyDef = b2DefaultBodyDef(); + bodyDef.type = b2_dynamicBody; + + bodyDef.position.x = x + offset * i; + bodyDef.position.y = h + 2.0f * h * i; + + // this tests the deterministic cosine and sine functions + bodyDef.rotation = b2MakeRot( 0.1f * i - 1.0f ); + + b2BodyId bodyId = b2CreateBody( m_worldId, &bodyDef ); + + if ((i & 1) == 0) + { + prevBodyId = bodyId; + } + else + { + jointDef.bodyIdA = prevBodyId; + jointDef.bodyIdB = bodyId; + b2CreateRevoluteJoint( m_worldId, &jointDef ); + prevBodyId = b2_nullBodyId; + } + + b2CreatePolygonShape( bodyId, &shapeDef, &box ); + + assert( bodyIndex < bodyCount ); + m_bodies[bodyIndex] = bodyId; + + bodyIndex += 1; + } + } + + m_hash = 0; + m_sleepStep = -1; + + //PrintTransforms(); + } + + void PrintTransforms() + { + uint32_t hash = B2_HASH_INIT; + int bodyCount = e_rows * e_columns; + for ( int i = 0; i < bodyCount; ++i ) + { + b2Transform xf = b2Body_GetTransform( m_bodies[i] ); + printf( "%d %.9f %.9f %.9f %.9f\n", i, xf.p.x, xf.p.y, xf.q.c, xf.q.s ); + hash = b2Hash( hash, reinterpret_cast( &xf ), sizeof( b2Transform ) ); + } + + printf( "hash = 0x%08x\n", hash ); + } + + void Step(Settings& settings) override + { + Sample::Step( settings ); + + if (m_hash == 0) + { + bool sleeping = true; + int bodyCount = e_rows * e_columns; + for (int i = 0; i < bodyCount; ++i) + { + if ( b2Body_IsAwake( m_bodies[i] ) == true ) + { + sleeping = false; + break; + } + } + + if (sleeping == true) + { + uint32_t hash = B2_HASH_INIT; + for ( int i = 0; i < bodyCount; ++i ) + { + b2Transform xf = b2Body_GetTransform( m_bodies[i] ); + //printf( "%d %.9f %.9f %.9f %.9f\n", i, xf.p.x, xf.p.y, xf.q.c, xf.q.s ); + hash = b2Hash( hash, reinterpret_cast( &xf ), sizeof( b2Transform ) ); + } + + m_sleepStep = m_stepCount - 1; + m_hash = hash; + printf( "sleep step = %d, hash = 0x%08x\n", m_sleepStep, m_hash ); + } + } + + g_draw.DrawString( 5, m_textLine, "sleep step = %d, hash = 0x%08x", m_sleepStep, m_hash ); + m_textLine += m_textIncrement; + } + + static Sample* Create( Settings& settings ) + { + return new FallingHinges( settings ); + } + + b2BodyId m_bodies[e_rows * e_columns]; + uint32_t m_hash; + int m_sleepStep; +}; + +static int sampleFallingHinges = RegisterSample( "Determinism", "Falling Hinges", FallingHinges::Create ); diff --git a/samples/sample_events.cpp b/samples/sample_events.cpp index 8d2fa3032..b7e6ff23c 100644 --- a/samples/sample_events.cpp +++ b/samples/sample_events.cpp @@ -130,7 +130,7 @@ class SensorEvent : public Sample } { - b2Polygon box = b2MakeOffsetBox( 4.0f, 1.0f, { 0.0f, -30.5f }, 0.0f ); + b2Polygon box = b2MakeOffsetBox( 4.0f, 1.0f, { 0.0f, -30.5f }, b2Rot_identity ); b2ShapeDef shapeDef = b2DefaultShapeDef(); shapeDef.isSensor = true; b2CreatePolygonShape( groundId, &shapeDef, &box ); @@ -927,21 +927,21 @@ class BodyMove : public Sample b2ShapeDef shapeDef = b2DefaultShapeDef(); shapeDef.friction = 0.1f; - b2Polygon box = b2MakeOffsetBox( 12.0f, 0.1f, { -10.0f, -0.1f }, -0.15f * b2_pi ); + b2Polygon box = b2MakeOffsetBox( 12.0f, 0.1f, { -10.0f, -0.1f }, b2MakeRot(-0.15f * b2_pi) ); b2CreatePolygonShape( groundId, &shapeDef, &box ); - box = b2MakeOffsetBox( 12.0f, 0.1f, { 10.0f, -0.1f }, 0.15f * b2_pi ); + box = b2MakeOffsetBox( 12.0f, 0.1f, { 10.0f, -0.1f }, b2MakeRot(0.15f * b2_pi) ); b2CreatePolygonShape( groundId, &shapeDef, &box ); shapeDef.restitution = 0.8f; - box = b2MakeOffsetBox( 0.1f, 10.0f, { 19.9f, 10.0f }, 0.0f ); + box = b2MakeOffsetBox( 0.1f, 10.0f, { 19.9f, 10.0f }, b2Rot_identity ); b2CreatePolygonShape( groundId, &shapeDef, &box ); - box = b2MakeOffsetBox( 0.1f, 10.0f, { -19.9f, 10.0f }, 0.0f ); + box = b2MakeOffsetBox( 0.1f, 10.0f, { -19.9f, 10.0f }, b2Rot_identity ); b2CreatePolygonShape( groundId, &shapeDef, &box ); - box = b2MakeOffsetBox( 20.0f, 0.1f, { 0.0f, 20.1f }, 0.0f ); + box = b2MakeOffsetBox( 20.0f, 0.1f, { 0.0f, 20.1f }, b2Rot_identity ); b2CreatePolygonShape( groundId, &shapeDef, &box ); } diff --git a/samples/sample_joints.cpp b/samples/sample_joints.cpp index 1c2263671..b3d452e64 100644 --- a/samples/sample_joints.cpp +++ b/samples/sample_joints.cpp @@ -433,7 +433,7 @@ class RevoluteJoint : public Sample bodyDef.type = b2_dynamicBody; b2BodyId body = b2CreateBody( m_worldId, &bodyDef ); - b2Polygon box = b2MakeOffsetBox( 10.0f, 0.5f, { -10.0f, 0.0f }, 0.0f ); + b2Polygon box = b2MakeOffsetBox( 10.0f, 0.5f, { -10.0f, 0.0f }, b2Rot_identity ); b2ShapeDef shapeDef = b2DefaultShapeDef(); shapeDef.density = 1.0f; b2CreatePolygonShape( body, &shapeDef, &box ); @@ -1778,7 +1778,7 @@ class UserConstraint : public Sample b2Vec2 localAnchors[2] = { { 1.0f, -0.5f }, { 1.0f, 0.5f } }; float mass = b2Body_GetMass( m_bodyId ); float invMass = mass < 0.0001f ? 0.0f : 1.0f / mass; - float inertiaTensor = b2Body_GetInertiaTensor( m_bodyId ); + float inertiaTensor = b2Body_GetRotationalInertia( m_bodyId ); float invI = inertiaTensor < 0.0001f ? 0.0f : 1.0f / inertiaTensor; b2Vec2 vB = b2Body_GetLinearVelocity( m_bodyId ); @@ -2224,7 +2224,7 @@ class DoohickeyFarm : public Sample b2Segment segment = { { -20.0f, 0.0f }, { 20.0f, 0.0f } }; b2CreateSegmentShape( groundId, &shapeDef, &segment ); - b2Polygon box = b2MakeOffsetBox( 1.0f, 1.0f, { 0.0f, 1.0f }, 0.0f ); + b2Polygon box = b2MakeOffsetBox( 1.0f, 1.0f, { 0.0f, 1.0f }, b2Rot_identity ); b2CreatePolygonShape( groundId, &shapeDef, &box ); } diff --git a/samples/sample_robustness.cpp b/samples/sample_robustness.cpp index 601303833..5d997f219 100644 --- a/samples/sample_robustness.cpp +++ b/samples/sample_robustness.cpp @@ -29,7 +29,7 @@ class HighMassRatio1 : public Sample b2BodyDef bodyDef = b2DefaultBodyDef(); b2BodyId groundId = b2CreateBody( m_worldId, &bodyDef ); b2ShapeDef shapeDef = b2DefaultShapeDef(); - b2Polygon box = b2MakeOffsetBox( 50.0f, 1.0f, { 0.0f, -1.0f }, 0.0f ); + b2Polygon box = b2MakeOffsetBox( 50.0f, 1.0f, { 0.0f, -1.0f }, b2Rot_identity ); b2CreatePolygonShape( groundId, &shapeDef, &box ); } @@ -90,7 +90,7 @@ class HighMassRatio2 : public Sample b2BodyDef bodyDef = b2DefaultBodyDef(); b2BodyId groundId = b2CreateBody( m_worldId, &bodyDef ); b2ShapeDef shapeDef = b2DefaultShapeDef(); - b2Polygon box = b2MakeOffsetBox( 50.0f, 1.0f, { 0.0f, -1.0f }, 0.0f ); + b2Polygon box = b2MakeOffsetBox( 50.0f, 1.0f, { 0.0f, -1.0f }, b2Rot_identity ); b2CreatePolygonShape( groundId, &shapeDef, &box ); } diff --git a/samples/sample_shapes.cpp b/samples/sample_shapes.cpp index e610e130c..91df17c43 100644 --- a/samples/sample_shapes.cpp +++ b/samples/sample_shapes.cpp @@ -241,9 +241,9 @@ class CompoundShapes : public Sample m_table1Id = b2CreateBody( m_worldId, &bodyDef ); b2ShapeDef shapeDef = b2DefaultShapeDef(); - b2Polygon top = b2MakeOffsetBox( 3.0f, 0.5f, { 0.0f, 3.5f }, 0.0f ); - b2Polygon leftLeg = b2MakeOffsetBox( 0.5f, 1.5f, { -2.5f, 1.5f }, 0.0f ); - b2Polygon rightLeg = b2MakeOffsetBox( 0.5f, 1.5f, { 2.5f, 1.5f }, 0.0f ); + b2Polygon top = b2MakeOffsetBox( 3.0f, 0.5f, { 0.0f, 3.5f }, b2Rot_identity ); + b2Polygon leftLeg = b2MakeOffsetBox( 0.5f, 1.5f, { -2.5f, 1.5f }, b2Rot_identity ); + b2Polygon rightLeg = b2MakeOffsetBox( 0.5f, 1.5f, { 2.5f, 1.5f }, b2Rot_identity ); b2CreatePolygonShape( m_table1Id, &shapeDef, &top ); b2CreatePolygonShape( m_table1Id, &shapeDef, &leftLeg ); @@ -258,9 +258,9 @@ class CompoundShapes : public Sample m_table2Id = b2CreateBody( m_worldId, &bodyDef ); b2ShapeDef shapeDef = b2DefaultShapeDef(); - b2Polygon top = b2MakeOffsetBox( 3.0f, 0.5f, { 0.0f, 3.5f }, 0.0f ); - b2Polygon leftLeg = b2MakeOffsetBox( 0.5f, 2.0f, { -2.5f, 2.0f }, 0.0f ); - b2Polygon rightLeg = b2MakeOffsetBox( 0.5f, 2.0f, { 2.5f, 2.0f }, 0.0f ); + b2Polygon top = b2MakeOffsetBox( 3.0f, 0.5f, { 0.0f, 3.5f }, b2Rot_identity ); + b2Polygon leftLeg = b2MakeOffsetBox( 0.5f, 2.0f, { -2.5f, 2.0f }, b2Rot_identity ); + b2Polygon rightLeg = b2MakeOffsetBox( 0.5f, 2.0f, { 2.5f, 2.0f }, b2Rot_identity ); b2CreatePolygonShape( m_table2Id, &shapeDef, &top ); b2CreatePolygonShape( m_table2Id, &shapeDef, &leftLeg ); @@ -333,7 +333,7 @@ class CompoundShapes : public Sample b2BodyId bodyId = b2CreateBody( m_worldId, &bodyDef ); b2ShapeDef shapeDef = b2DefaultShapeDef(); - b2Polygon box = b2MakeOffsetBox( 4.0f, 0.1f, { 0.0f, 3.0f }, 0.0f ); + b2Polygon box = b2MakeOffsetBox( 4.0f, 0.1f, { 0.0f, 3.0f }, b2Rot_identity ); b2CreatePolygonShape( bodyId, &shapeDef, &box ); } @@ -346,7 +346,7 @@ class CompoundShapes : public Sample b2BodyId bodyId = b2CreateBody( m_worldId, &bodyDef ); b2ShapeDef shapeDef = b2DefaultShapeDef(); - b2Polygon box = b2MakeOffsetBox( 4.0f, 0.1f, { 0.0f, 3.0f }, 0.0f ); + b2Polygon box = b2MakeOffsetBox( 4.0f, 0.1f, { 0.0f, 3.0f }, b2Rot_identity ); b2CreatePolygonShape( bodyId, &shapeDef, &box ); } @@ -888,19 +888,19 @@ class Friction : public Sample b2Segment segment = { { -40.0f, 0.0f }, { 40.0f, 0.0f } }; b2CreateSegmentShape( groundId, &shapeDef, &segment ); - b2Polygon box = b2MakeOffsetBox( 13.0f, 0.25f, { -4.0f, 22.0f }, -0.25f ); + b2Polygon box = b2MakeOffsetBox( 13.0f, 0.25f, { -4.0f, 22.0f }, b2MakeRot(-0.25f) ); b2CreatePolygonShape( groundId, &shapeDef, &box ); - box = b2MakeOffsetBox( 0.25f, 1.0f, { 10.5f, 19.0f }, 0.0f ); + box = b2MakeOffsetBox( 0.25f, 1.0f, { 10.5f, 19.0f }, b2Rot_identity ); b2CreatePolygonShape( groundId, &shapeDef, &box ); - box = b2MakeOffsetBox( 13.0f, 0.25f, { 4.0f, 14.0f }, 0.25f ); + box = b2MakeOffsetBox( 13.0f, 0.25f, { 4.0f, 14.0f }, b2MakeRot(0.25f) ); b2CreatePolygonShape( groundId, &shapeDef, &box ); - box = b2MakeOffsetBox( 0.25f, 1.0f, { -10.5f, 11.0f }, 0.0f ); + box = b2MakeOffsetBox( 0.25f, 1.0f, { -10.5f, 11.0f }, b2Rot_identity ); b2CreatePolygonShape( groundId, &shapeDef, &box ); - box = b2MakeOffsetBox( 13.0f, 0.25f, { -4.0f, 6.0f }, -0.25f ); + box = b2MakeOffsetBox( 13.0f, 0.25f, { -4.0f, 6.0f }, b2MakeRot(-0.25f) ); b2CreatePolygonShape( groundId, &shapeDef, &box ); } @@ -951,7 +951,7 @@ class ModifyGeometry : public Sample b2BodyDef bodyDef = b2DefaultBodyDef(); b2BodyId groundId = b2CreateBody( m_worldId, &bodyDef ); b2ShapeDef shapeDef = b2DefaultShapeDef(); - b2Polygon box = b2MakeOffsetBox( 10.0f, 1.0f, { 0.0f, -1.0f }, 0.0f ); + b2Polygon box = b2MakeOffsetBox( 10.0f, 1.0f, { 0.0f, -1.0f }, b2Rot_identity ); b2CreatePolygonShape( groundId, &shapeDef, &box ); } @@ -1193,13 +1193,13 @@ class RoundedShapes : public Sample b2BodyId groundId = b2CreateBody( m_worldId, &bodyDef ); b2ShapeDef shapeDef = b2DefaultShapeDef(); - b2Polygon box = b2MakeOffsetBox( 20.0f, 1.0f, { 0.0f, -1.0f }, 0.0f ); + b2Polygon box = b2MakeOffsetBox( 20.0f, 1.0f, { 0.0f, -1.0f }, b2Rot_identity ); b2CreatePolygonShape( groundId, &shapeDef, &box ); - box = b2MakeOffsetBox( 1.0f, 5.0f, { 19.0f, 5.0f }, 0.0f ); + box = b2MakeOffsetBox( 1.0f, 5.0f, { 19.0f, 5.0f }, b2Rot_identity ); b2CreatePolygonShape( groundId, &shapeDef, &box ); - box = b2MakeOffsetBox( 1.0f, 5.0f, { -19.0f, 5.0f }, 0.0f ); + box = b2MakeOffsetBox( 1.0f, 5.0f, { -19.0f, 5.0f }, b2Rot_identity ); b2CreatePolygonShape( groundId, &shapeDef, &box ); } @@ -1263,7 +1263,7 @@ class OffsetShapes : public Sample b2BodyId groundId = b2CreateBody( m_worldId, &bodyDef ); b2ShapeDef shapeDef = b2DefaultShapeDef(); - b2Polygon box = b2MakeOffsetBox( 1.0f, 1.0f, { 10.0f, -2.0f }, 0.5f * b2_pi ); + b2Polygon box = b2MakeOffsetBox( 1.0f, 1.0f, { 10.0f, -2.0f }, b2MakeRot(0.5f * b2_pi) ); b2CreatePolygonShape( groundId, &shapeDef, &box ); } @@ -1278,7 +1278,7 @@ class OffsetShapes : public Sample } { - b2Polygon box = b2MakeOffsetBox( 0.75f, 0.5f, { 9.0f, 2.0f }, 0.5f * b2_pi ); + b2Polygon box = b2MakeOffsetBox( 0.75f, 0.5f, { 9.0f, 2.0f }, b2MakeRot(0.5f * b2_pi) ); b2BodyDef bodyDef = b2DefaultBodyDef(); bodyDef.position = { 0.0f, 0.0f }; bodyDef.type = b2_dynamicBody; diff --git a/samples/sample_stacking.cpp b/samples/sample_stacking.cpp index 543b99b90..13cc25752 100644 --- a/samples/sample_stacking.cpp +++ b/samples/sample_stacking.cpp @@ -454,13 +454,13 @@ class Cliff : public Sample b2BodyId groundId = b2CreateBody( m_worldId, &bodyDef ); b2ShapeDef shapeDef = b2DefaultShapeDef(); - b2Polygon box = b2MakeOffsetBox( 100.0f, 1.0f, { 0.0f, -1.0f }, 0.0f ); + b2Polygon box = b2MakeOffsetBox( 100.0f, 1.0f, { 0.0f, -1.0f }, b2Rot_identity ); b2CreatePolygonShape( groundId, &shapeDef, &box ); b2Segment segment = { { -14.0f, 4.0f }, { -8.0f, 4.0f } }; b2CreateSegmentShape( groundId, &shapeDef, &segment ); - box = b2MakeOffsetBox( 3.0f, 0.5f, { 0.0f, 4.0f }, 0.0f ); + box = b2MakeOffsetBox( 3.0f, 0.5f, { 0.0f, 4.0f }, b2Rot_identity ); b2CreatePolygonShape( groundId, &shapeDef, &box ); b2Capsule capsule = { { 8.5f, 4.0f }, { 13.5f, 4.0f }, 0.5f }; diff --git a/samples/sample_world.cpp b/samples/sample_world.cpp index 9cc585f6c..1e5ad87be 100644 --- a/samples/sample_world.cpp +++ b/samples/sample_world.cpp @@ -71,7 +71,7 @@ class LargeWorld : public Sample for ( int j = 0; j < ycount; ++j ) { - b2Polygon square = b2MakeOffsetBox( 0.4f * m_gridSize, 0.4f * m_gridSize, { xShape, y }, 0.0f ); + b2Polygon square = b2MakeOffsetBox( 0.4f * m_gridSize, 0.4f * m_gridSize, { xShape, y }, b2Rot_identity ); square.radius = 0.1f; b2CreatePolygonShape( groundId, &shapeDef, &square ); diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 23437f04d..db1b331d6 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -149,19 +149,12 @@ elseif (MINGW) target_compile_definitions(box2d PRIVATE BOX2D_AVX2) target_compile_options(box2d PRIVATE -mavx2) else() - # todo should no longer be needed - # see SIMDE_DIAGNOSTIC_DISABLE_PSABI_ - message(STATUS "Box2D disabling ABI warning") - target_compile_options(box2d PRIVATE -Wno-psabi) endif() elseif (APPLE) message(STATUS "Box2D on Apple") + target_compile_options(box2d PRIVATE -ffp-model=precise -ffp-contract=off) elseif (EMSCRIPTEN) message(STATUS "Box2D on Emscripten") - # todo should no longer be needed - # see SIMDE_DIAGNOSTIC_DISABLE_PSABI_ - message(STATUS "Box2D disabling ABI warning") - target_compile_options(box2d PRIVATE -Wno-psabi) elseif (UNIX) message(STATUS "Box2D using Unix") if ("${CMAKE_HOST_SYSTEM_PROCESSOR}" STREQUAL "aarch64") @@ -176,10 +169,6 @@ elseif (UNIX) target_compile_definitions(box2d PRIVATE BOX2D_AVX2) target_compile_options(box2d PRIVATE -mavx2) else() - # todo should no longer be needed - # see SIMDE_DIAGNOSTIC_DISABLE_PSABI_ - message(STATUS "Box2D disabling ABI warning") - target_compile_options(box2d PRIVATE -Wno-psabi) endif() endif() endif() diff --git a/src/aabb.h b/src/aabb.h index 234e722f0..6fa99c4ea 100644 --- a/src/aabb.h +++ b/src/aabb.h @@ -48,13 +48,6 @@ static inline bool b2EnlargeAABB( b2AABB* a, b2AABB b ) return changed; } -static inline bool b2AABB_ContainsWithMargin( b2AABB a, b2AABB b, float margin ) -{ - bool s = ( a.lowerBound.x <= b.lowerBound.x - margin ) & ( a.lowerBound.y <= b.lowerBound.y - margin ) & - ( b.upperBound.x + margin <= a.upperBound.x ) & ( b.upperBound.y + margin <= a.upperBound.y ); - return s; -} - /// Do a and b overlap static inline bool b2AABB_Overlaps( b2AABB a, b2AABB b ) { diff --git a/src/body.c b/src/body.c index 2b9e2804f..2297a7c09 100644 --- a/src/body.c +++ b/src/body.c @@ -1161,14 +1161,15 @@ void b2Body_SetType( b2BodyId bodyId, b2BodyType type ) continue; } - b2LinkJoint( world, joint ); + b2LinkJoint( world, joint, false); } + + b2MergeAwakeIslands( world ); } // Body type affects the mass b2UpdateBodyMassData( world, body ); - b2ValidateConnectivity( world ); b2ValidateSolverSets( world ); } @@ -1194,7 +1195,7 @@ float b2Body_GetMass( b2BodyId bodyId ) return bodySim->mass; } -float b2Body_GetInertiaTensor( b2BodyId bodyId ) +float b2Body_GetRotationalInertia( b2BodyId bodyId ) { b2World* world = b2GetWorld( bodyId.world0 ); b2Body* body = b2GetBodyFullId( world, bodyId ); @@ -1404,11 +1405,11 @@ bool b2Body_IsSleepEnabled( b2BodyId bodyId ) return body->enableSleep; } -void b2Body_SetSleepThreshold( b2BodyId bodyId, float sleepVelocity ) +void b2Body_SetSleepThreshold( b2BodyId bodyId, float sleepThreshold ) { b2World* world = b2GetWorld( bodyId.world0 ); b2Body* body = b2GetBodyFullId( world, bodyId ); - body->sleepThreshold = sleepVelocity; + body->sleepThreshold = sleepThreshold; } float b2Body_GetSleepThreshold( b2BodyId bodyId ) @@ -1551,6 +1552,7 @@ void b2Body_Enable( b2BodyId bodyId ) // Transfer joints. If the other body is disabled, don't transfer. // If the other body is sleeping, wake it. + bool mergeIslands = false; int jointKey = body->headJointKey; while ( jointKey != B2_NULL_INDEX ) { @@ -1594,11 +1596,13 @@ void b2Body_Enable( b2BodyId bodyId ) // Now that the joint is in the correct set, I can link the joint in the island. if ( jointSetId != b2_staticSet ) { - b2LinkJoint( world, joint ); + b2LinkJoint( world, joint, mergeIslands ); } } - b2ValidateConnectivity( world ); + // Now merge islands + b2MergeAwakeIslands( world ); + b2ValidateSolverSets( world ); } diff --git a/src/broad_phase.c b/src/broad_phase.c index 8e144abe4..ced9bca23 100644 --- a/src/broad_phase.c +++ b/src/broad_phase.c @@ -88,7 +88,7 @@ static inline void b2UnBufferMove( b2BroadPhase* bp, int proxyKey ) } } -int b2BroadPhase_CreateProxy( b2BroadPhase* bp, b2BodyType proxyType, b2AABB aabb, uint32_t categoryBits, int shapeIndex, +int b2BroadPhase_CreateProxy( b2BroadPhase* bp, b2BodyType proxyType, b2AABB aabb, uint64_t categoryBits, int shapeIndex, bool forcePairCreation ) { B2_ASSERT( 0 <= proxyType && proxyType < b2_bodyTypeCount ); @@ -312,7 +312,8 @@ void b2FindPairsTask( int startIndex, int endIndex, uint32_t threadIndex, void* b2AABB fatAABB = b2DynamicTree_GetAABB( baseTree, proxyId ); queryContext.queryShapeIndex = b2DynamicTree_GetUserData( baseTree, proxyId ); - // Query trees. Only dynamic proxies collide with kinematic and static proxies + // Query trees. Only dynamic proxies collide with kinematic and static proxies. + // Using b2_defaultMaskBits so that b2Filter::groupIndex works. if ( proxyType == b2_dynamicBody ) { queryContext.queryTreeType = b2_kinematicBody; @@ -323,6 +324,7 @@ void b2FindPairsTask( int startIndex, int endIndex, uint32_t threadIndex, void* } // All proxies collide with dynamic proxies + // Using b2_defaultMaskBits so that b2Filter::groupIndex works. queryContext.queryTreeType = b2_dynamicBody; b2DynamicTree_Query( bp->trees + b2_dynamicBody, fatAABB, b2_defaultMaskBits, b2PairQueryCallback, &queryContext ); } diff --git a/src/broad_phase.h b/src/broad_phase.h index b1c8dda27..07284ab26 100644 --- a/src/broad_phase.h +++ b/src/broad_phase.h @@ -53,7 +53,7 @@ typedef struct b2BroadPhase void b2CreateBroadPhase( b2BroadPhase* bp ); void b2DestroyBroadPhase( b2BroadPhase* bp ); -int b2BroadPhase_CreateProxy( b2BroadPhase* bp, b2BodyType proxyType, b2AABB aabb, uint32_t categoryBits, int shapeIndex, +int b2BroadPhase_CreateProxy( b2BroadPhase* bp, b2BodyType proxyType, b2AABB aabb, uint64_t categoryBits, int shapeIndex, bool forcePairCreation ); void b2BroadPhase_DestroyProxy( b2BroadPhase* bp, int proxyKey ); diff --git a/src/core.c b/src/core.c index c18ac2047..9264dce54 100644 --- a/src/core.c +++ b/src/core.c @@ -38,5 +38,5 @@ void b2SetAssertFcn( b2AssertFcn* assertFcn ) b2Version b2GetVersion( void ) { - return ( b2Version ){ 3, 0, 1 }; + return ( b2Version ){ 3, 1, 0 }; } diff --git a/src/core.h b/src/core.h index 4e6aa27a4..62909202a 100644 --- a/src/core.h +++ b/src/core.h @@ -5,142 +5,134 @@ #include "box2d/base.h" +// clang-format off + #define B2_NULL_INDEX ( -1 ) #ifdef NDEBUG -#define B2_DEBUG 0 + #define B2_DEBUG 0 #else -#define B2_DEBUG 1 + #define B2_DEBUG 1 #endif #if defined( BOX2D_VALIDATE ) && !defined( NDEBUG ) -#define B2_VALIDATE 1 + #define B2_VALIDATE 1 #else -#define B2_VALIDATE 0 + #define B2_VALIDATE 0 #endif // Define platform #if defined( _WIN64 ) -#define B2_PLATFORM_WINDOWS + #define B2_PLATFORM_WINDOWS #elif defined( __ANDROID__ ) -#define B2_PLATFORM_ANDROID + #define B2_PLATFORM_ANDROID #elif defined( __linux__ ) -#define B2_PLATFORM_LINUX + #define B2_PLATFORM_LINUX #elif defined( __APPLE__ ) -#include -#if defined( TARGET_OS_IPHONE ) && !TARGET_OS_IPHONE -#define B2_PLATFORM_MACOS -#else -#define B2_PLATFORM_IOS -#endif + #include + #if defined( TARGET_OS_IPHONE ) && !TARGET_OS_IPHONE + #define B2_PLATFORM_MACOS + #else + #define B2_PLATFORM_IOS + #endif #elif defined( __EMSCRIPTEN__ ) -#define B2_PLATFORM_WASM + #define B2_PLATFORM_WASM #else -#error Unsupported platform + #error Unsupported platform #endif // Define CPU #if defined( __x86_64__ ) || defined( _M_X64 ) -#define B2_CPU_X64 + #define B2_CPU_X64 #elif defined( __aarch64__ ) || defined( _M_ARM64 ) -#define B2_CPU_ARM + #define B2_CPU_ARM #elif defined( __EMSCRIPTEN__ ) -#define B2_CPU_WASM + #define B2_CPU_WASM #else -#error Unsupported CPU + #error Unsupported CPU #endif // Define SIMD #if defined( BOX2D_ENABLE_SIMD ) - -#if defined( B2_CPU_X64 ) - -#if defined( BOX2D_AVX2 ) -#define B2_SIMD_AVX2 -#define B2_SIMD_WIDTH 8 -#else -#define B2_SIMD_SSE2 -#define B2_SIMD_WIDTH 4 -#endif - -#elif defined( B2_CPU_ARM ) - -#define B2_SIMD_NEON -#define B2_SIMD_WIDTH 4 - -#elif defined( __EMSCRIPTEN__ ) - -#define B2_CPU_WASM -#define B2_SIMD_SSE2 -#define B2_SIMD_WIDTH 4 - + #if defined( B2_CPU_X64 ) + #if defined( BOX2D_AVX2 ) + #define B2_SIMD_AVX2 + #define B2_SIMD_WIDTH 8 + #else + #define B2_SIMD_SSE2 + #define B2_SIMD_WIDTH 4 + #endif + #elif defined( B2_CPU_ARM ) + #define B2_SIMD_NEON + #define B2_SIMD_WIDTH 4 + #elif defined( __EMSCRIPTEN__ ) + #define B2_CPU_WASM + #define B2_SIMD_SSE2 + #define B2_SIMD_WIDTH 4 + #else + #define B2_SIMD_NONE + #define B2_SIMD_WIDTH 4 + #endif #else - -#define B2_SIMD_NONE -#define B2_SIMD_WIDTH 4 - -#endif - -#else - -#define B2_SIMD_NONE -#define B2_SIMD_WIDTH 4 - + #define B2_SIMD_NONE + #define B2_SIMD_WIDTH 4 #endif // Define compiler #if defined( __clang__ ) -#define B2_COMPILER_CLANG + #define B2_COMPILER_CLANG #elif defined( __GNUC__ ) -#define B2_COMPILER_GCC + #define B2_COMPILER_GCC #elif defined( _MSC_VER ) -#define B2_COMPILER_MSVC + #define B2_COMPILER_MSVC #endif #if defined( B2_COMPILER_MSVC ) -#define B2_BREAKPOINT __debugbreak() + #define B2_BREAKPOINT __debugbreak() #elif defined( B2_PLATFORM_WASM ) -#define B2_BREAKPOINT \ - do \ - { \ - } \ - while ( 0 ) + #define B2_BREAKPOINT \ + do \ + { \ + } \ + while ( 0 ) #elif defined( B2_COMPILER_GCC ) || defined( B2_COMPILER_CLANG ) -#if defined( B2_CPU_X64 ) -#define B2_BREAKPOINT __asm volatile( "int $0x3" ) -#elif defined( B2_CPU_ARM ) -#define B2_BREAKPOINT __builtin_trap() -#endif + #if defined( B2_CPU_X64 ) + #define B2_BREAKPOINT __asm volatile( "int $0x3" ) + #elif defined( B2_CPU_ARM ) + #define B2_BREAKPOINT __builtin_trap() + #endif #else -#error Unknown platform + #error Unknown platform #endif #if !defined( NDEBUG ) || defined( B2_ENABLE_ASSERT ) -extern b2AssertFcn* b2AssertHandler; -#define B2_ASSERT( condition ) \ - do \ - { \ - if ( !( condition ) && b2AssertHandler( #condition, __FILE__, (int)__LINE__ ) ) \ - B2_BREAKPOINT; \ - } \ - while ( 0 ) + extern b2AssertFcn* b2AssertHandler; + #define B2_ASSERT( condition ) \ + do \ + { \ + if ( !( condition ) && b2AssertHandler( #condition, __FILE__, (int)__LINE__ ) ) \ + B2_BREAKPOINT; \ + } \ + while ( 0 ) #else -#define B2_ASSERT( ... ) ( (void)0 ) + #define B2_ASSERT( ... ) ( (void)0 ) #endif /// Tracy profiler instrumentation /// https://github.com/wolfpld/tracy #ifdef BOX2D_PROFILE -#include -#define b2TracyCZoneC( ctx, color, active ) TracyCZoneC( ctx, color, active ) -#define b2TracyCZoneNC( ctx, name, color, active ) TracyCZoneNC( ctx, name, color, active ) -#define b2TracyCZoneEnd( ctx ) TracyCZoneEnd( ctx ) + #include + #define b2TracyCZoneC( ctx, color, active ) TracyCZoneC( ctx, color, active ) + #define b2TracyCZoneNC( ctx, name, color, active ) TracyCZoneNC( ctx, name, color, active ) + #define b2TracyCZoneEnd( ctx ) TracyCZoneEnd( ctx ) #else -#define b2TracyCZoneC( ctx, color, active ) -#define b2TracyCZoneNC( ctx, name, color, active ) -#define b2TracyCZoneEnd( ctx ) + #define b2TracyCZoneC( ctx, color, active ) + #define b2TracyCZoneNC( ctx, name, color, active ) + #define b2TracyCZoneEnd( ctx ) #endif +// clang-format on + extern float b2_lengthUnitsPerMeter; // Used to detect bad values. Positions greater than about 16km will have precision diff --git a/src/distance_joint.c b/src/distance_joint.c index 6e86e662b..9a889c245 100644 --- a/src/distance_joint.c +++ b/src/distance_joint.c @@ -119,14 +119,14 @@ void b2DistanceJoint_SetSpringDampingRatio( b2JointId jointId, float dampingRati base->distanceJoint.dampingRatio = dampingRatio; } -float b2DistanceJoint_GetHertz( b2JointId jointId ) +float b2DistanceJoint_GetSpringHertz( b2JointId jointId ) { b2JointSim* base = b2GetJointSimCheckType( jointId, b2_distanceJoint ); b2DistanceJoint* joint = &base->distanceJoint; return joint->hertz; } -float b2DistanceJoint_GetDampingRatio( b2JointId jointId ) +float b2DistanceJoint_GetSpringDampingRatio( b2JointId jointId ) { b2JointSim* base = b2GetJointSimCheckType( jointId, b2_distanceJoint ); b2DistanceJoint* joint = &base->distanceJoint; diff --git a/src/dynamic_tree.c b/src/dynamic_tree.c index 1f76f4426..9f30f0341 100644 --- a/src/dynamic_tree.c +++ b/src/dynamic_tree.c @@ -18,7 +18,7 @@ static b2TreeNode b2_defaultTreeNode = { { { 0.0f, 0.0f }, { 0.0f, 0.0f } }, 0, { B2_NULL_INDEX }, B2_NULL_INDEX, B2_NULL_INDEX, -1, -2, false, - { 0, 0, 0, 0, 0, 0, 0, 0, 0 } }; + { 0, 0, 0, 0, 0 } }; static inline bool b2IsLeaf( const b2TreeNode* node ) { @@ -726,7 +726,7 @@ static void b2RemoveLeaf( b2DynamicTree* tree, int32_t leaf ) // Create a proxy in the tree as a leaf node. We return the index of the node instead of a pointer so that we can grow // the node pool. -int32_t b2DynamicTree_CreateProxy( b2DynamicTree* tree, b2AABB aabb, uint32_t categoryBits, int32_t userData ) +int32_t b2DynamicTree_CreateProxy( b2DynamicTree* tree, b2AABB aabb, uint64_t categoryBits, int32_t userData ) { B2_ASSERT( -b2_huge < aabb.lowerBound.x && aabb.lowerBound.x < b2_huge ); B2_ASSERT( -b2_huge < aabb.lowerBound.y && aabb.lowerBound.y < b2_huge ); @@ -961,7 +961,7 @@ static void b2ValidateMetrics( const b2DynamicTree* tree, int32_t index ) // B2_ASSERT(aabb.upperBound.x == node->aabb.upperBound.x); // B2_ASSERT(aabb.upperBound.y == node->aabb.upperBound.y); - uint32_t categoryBits = tree->nodes[child1].categoryBits | tree->nodes[child2].categoryBits; + uint64_t categoryBits = tree->nodes[child1].categoryBits | tree->nodes[child2].categoryBits; B2_ASSERT( node->categoryBits == categoryBits ); b2ValidateMetrics( tree, child1 ); @@ -1118,7 +1118,7 @@ int b2DynamicTree_GetByteCount( const b2DynamicTree* tree ) return (int)size; } -void b2DynamicTree_Query( const b2DynamicTree* tree, b2AABB aabb, uint32_t maskBits, b2TreeQueryCallbackFcn* callback, +void b2DynamicTree_Query( const b2DynamicTree* tree, b2AABB aabb, uint64_t maskBits, b2TreeQueryCallbackFcn* callback, void* context ) { int32_t stack[b2_treeStackSize]; @@ -1159,7 +1159,7 @@ void b2DynamicTree_Query( const b2DynamicTree* tree, b2AABB aabb, uint32_t maskB } } -void b2DynamicTree_RayCast( const b2DynamicTree* tree, const b2RayCastInput* input, uint32_t maskBits, +void b2DynamicTree_RayCast( const b2DynamicTree* tree, const b2RayCastInput* input, uint64_t maskBits, b2TreeRayCastCallbackFcn* callback, void* context ) { b2Vec2 p1 = input->origin; @@ -1248,7 +1248,7 @@ void b2DynamicTree_RayCast( const b2DynamicTree* tree, const b2RayCastInput* inp } } -void b2DynamicTree_ShapeCast( const b2DynamicTree* tree, const b2ShapeCastInput* input, uint32_t maskBits, +void b2DynamicTree_ShapeCast( const b2DynamicTree* tree, const b2ShapeCastInput* input, uint64_t maskBits, b2TreeShapeCastCallbackFcn* callback, void* context ) { if ( input->count == 0 ) diff --git a/src/geometry.c b/src/geometry.c index 03b054733..94d33d368 100644 --- a/src/geometry.c +++ b/src/geometry.c @@ -156,11 +156,11 @@ b2Polygon b2MakeRoundedBox( float hx, float hy, float radius ) return shape; } -b2Polygon b2MakeOffsetBox( float hx, float hy, b2Vec2 center, float angle ) +b2Polygon b2MakeOffsetBox( float hx, float hy, b2Vec2 center, b2Rot rotation ) { b2Transform xf; xf.p = center; - xf.q = b2MakeRot( angle ); + xf.q = rotation; b2Polygon shape = { 0 }; shape.count = 4; diff --git a/src/island.c b/src/island.c index d84098801..c2c47313d 100644 --- a/src/island.c +++ b/src/island.c @@ -295,7 +295,7 @@ static void b2AddJointToIsland( b2World* world, int islandId, b2Joint* joint ) b2ValidateIsland( world, islandId ); } -void b2LinkJoint( b2World* world, b2Joint* joint ) +void b2LinkJoint( b2World* world, b2Joint* joint, bool mergeIslands ) { b2Body* bodyA = b2GetBody( world, joint->edges[0].bodyId ); b2Body* bodyB = b2GetBody( world, joint->edges[1].bodyId ); @@ -377,6 +377,15 @@ void b2LinkJoint( b2World* world, b2Joint* joint ) { b2AddJointToIsland( world, islandIdB, joint ); } + + // Joints need to have islands merged immediately when they are created + // to keep the island graph valid. + // However, when a body type is being changed the merge can be deferred until + // all joints are linked. + if (mergeIslands) + { + b2MergeAwakeIslands( world ); + } } void b2UnlinkJoint( b2World* world, b2Joint* joint ) diff --git a/src/island.h b/src/island.h index 2e0384c29..d465744d6 100644 --- a/src/island.h +++ b/src/island.h @@ -3,6 +3,7 @@ #pragma once +#include #include typedef struct b2Body b2Body; @@ -74,7 +75,7 @@ void b2LinkContact( b2World* world, b2Contact* contact ); void b2UnlinkContact( b2World* world, b2Contact* contact ); // Link a joint into the island graph when it is created -void b2LinkJoint( b2World* world, b2Joint* joint ); +void b2LinkJoint( b2World* world, b2Joint* joint, bool mergeIslands ); // Unlink a joint from the island graph when it is destroyed void b2UnlinkJoint( b2World* world, b2Joint* joint ); diff --git a/src/joint.c b/src/joint.c index 3080653cb..fb2bddd59 100644 --- a/src/joint.c +++ b/src/joint.c @@ -279,7 +279,8 @@ static b2JointPair b2CreateJoint( b2World* world, b2Body* bodyA, b2Body* bodyB, if ( joint->setIndex > b2_disabledSet ) { // Add edge to island graph - b2LinkJoint( world, joint ); + bool mergeIslands = true; + b2LinkJoint( world, joint, mergeIslands ); } b2ValidateSolverSets( world ); @@ -712,7 +713,15 @@ void b2DestroyJointInternal( b2World* world, b2Joint* joint, bool wakeBodies ) bodyB->jointCount -= 1; - b2UnlinkJoint( world, joint ); + if (joint->islandId != B2_NULL_INDEX) + { + B2_ASSERT( joint->setIndex > b2_disabledSet ); + b2UnlinkJoint( world, joint ); + } + else + { + B2_ASSERT( joint->setIndex <= b2_disabledSet ); + } // Remove joint from solver set that owns it int setIndex = joint->setIndex; diff --git a/src/manifold.c b/src/manifold.c index 88b98242d..cd46ee0b7 100644 --- a/src/manifold.c +++ b/src/manifold.c @@ -18,7 +18,9 @@ static b2Polygon b2MakeCapsule( b2Vec2 p1, b2Vec2 p2, float radius ) shape.vertices[1] = p2; shape.centroid = b2Lerp( p1, p2, 0.5f ); - b2Vec2 axis = b2NormalizeChecked( b2Sub( p2, p1 ) ); + b2Vec2 d = b2Sub( p2, p1 ); + B2_ASSERT( b2LengthSquared( d ) > FLT_EPSILON ); + b2Vec2 axis = b2Normalize(d); b2Vec2 normal = b2RightPerp( axis ); shape.normals[0] = normal; diff --git a/src/math_functions.c b/src/math_functions.c index d3b1c0471..3a22e5010 100644 --- a/src/math_functions.c +++ b/src/math_functions.c @@ -52,42 +52,96 @@ bool b2Rot_IsValid( b2Rot q ) return b2IsNormalized( q ); } -b2Vec2 b2Normalize( b2Vec2 v ) +// https://mazzo.li/posts/vectorized-atan2.html +static inline float b2Atan( float x ) { - float length = b2Length( v ); - if ( length < FLT_EPSILON ) - { - return b2Vec2_zero; - } - - float invLength = 1.0f / length; - b2Vec2 n = { invLength * v.x, invLength * v.y }; - return n; + float a1 = 0.99997726f; + float a3 = -0.33262347f; + float a5 = 0.19354346f; + float a7 = -0.11643287f; + float a9 = 0.05265332f; + float a11 = -0.01172120f; + + float x2 = x * x; + return x * ( a1 + x2 * ( a3 + x2 * ( a5 + x2 * ( a7 + x2 * ( a9 + x2 * a11 ) ) ) ) ); } -b2Vec2 b2NormalizeChecked( b2Vec2 v ) +// I tested atan2f and got different results on Apple Clang (Arm) than MSVC (x64). +float b2Atan2( float y, float x ) { - float length = b2Length( v ); - if ( length < FLT_EPSILON ) + float pi = b2_pi; + float halfPi = 0.5f * b2_pi; + + bool swap = b2AbsFloat( x ) < b2AbsFloat( y ); + float atanInput = ( swap ? x : y ) / ( swap ? y : x ); + + // Approximate atan + float res = b2Atan( atanInput ); + + // If swapped, adjust atan output + res = swap ? ( atanInput >= 0.0f ? halfPi : -halfPi ) - res : res; + // Adjust quadrants + if ( x >= 0.0f && y >= 0.0f ) { - B2_ASSERT( false ); - return b2Vec2_zero; - } + } // 1st quadrant + else if ( x < 0.0f && y >= 0.0f ) + { + res = pi + res; + } // 2nd quadrant + else if ( x < 0.0f && y < 0.0f ) + { + res = -pi + res; + } // 3rd quadrant + else if ( x >= 0.0f && y < 0.0f ) + { + } // 4th quadrant - float invLength = 1.0f / length; - b2Vec2 n = { invLength * v.x, invLength * v.y }; - return n; + return res; } -b2Vec2 b2GetLengthAndNormalize( float* length, b2Vec2 v ) +// Approximate cosine and sine for determinism. In my testing cosf and sinf produced +// the same results on x64 and ARM using MSVC, GCC, and Clang. However, I don't trust +// this result. +// https://en.wikipedia.org/wiki/Bh%C4%81skara_I%27s_sine_approximation_formula +b2Rot b2MakeRot(float angle) { - *length = b2Length( v ); - if ( *length < FLT_EPSILON ) + // return ( b2Rot ){ cosf( angle ), sinf( angle ) }; + + float x = b2UnwindLargeAngle( angle ); + float pi2 = b2_pi * b2_pi; + + b2Rot q; + + // cosine needs angle in [-pi/2, pi/2] + if (x < -0.5f * b2_pi) + { + float y = x + b2_pi; + float y2 = y * y; + q.c = -( pi2 - 4.0f * y2 ) / ( pi2 + y2 ); + } + else if (x > 0.5f * b2_pi) + { + float y = x - b2_pi; + float y2 = y * y; + q.c = -( pi2 - 4.0f * y2 ) / ( pi2 + y2 ); + } + else + { + float y2 = x * x; + q.c = ( pi2 - 4.0f * y2 ) / ( pi2 + y2 ); + } + + // sine needs angle in [0, pi] + if (x < 0.0f) + { + float y = x + b2_pi; + q.s = -16.0f * y * ( b2_pi - y ) / ( 5.0f * pi2 - 4.0f * y * ( b2_pi - y ) ); + } + else { - return b2Vec2_zero; + q.s = 16.0f * x * ( b2_pi - x ) / ( 5.0f * pi2 - 4.0f * x * ( b2_pi - x ) ); } - float invLength = 1.0f / *length; - b2Vec2 n = { invLength * v.x, invLength * v.y }; - return n; + q = b2NormalizeRot( q ); + return q; } diff --git a/src/revolute_joint.c b/src/revolute_joint.c index afa7858eb..03f49306f 100644 --- a/src/revolute_joint.c +++ b/src/revolute_joint.c @@ -512,7 +512,8 @@ void b2DrawRevoluteJoint( b2DebugDraw* draw, b2JointSim* base, b2Transform trans float angle = b2RelativeAngle( transformB.q, transformA.q ); - b2Vec2 r = { L * cosf( angle ), L * sinf( angle ) }; + b2Rot rot = b2MakeRot( angle ); + b2Vec2 r = { L * rot.c, L * rot.s }; b2Vec2 pC = b2Add( pB, r ); draw->DrawSegment( pB, pC, c1, draw->context ); @@ -529,13 +530,17 @@ void b2DrawRevoluteJoint( b2DebugDraw* draw, b2JointSim* base, b2Transform trans if ( joint->enableLimit ) { - b2Vec2 rlo = { L * cosf( lowerAngle ), L * sinf( lowerAngle ) }; - b2Vec2 rhi = { L * cosf( upperAngle ), L * sinf( upperAngle ) }; + b2Rot rotLo = b2MakeRot( lowerAngle ); + b2Vec2 rlo = { L * rotLo.c, L * rotLo.s }; + + b2Rot rotHi = b2MakeRot( upperAngle ); + b2Vec2 rhi = { L * rotHi.c, L * rotHi.s }; draw->DrawSegment( pB, b2Add( pB, rlo ), c2, draw->context ); draw->DrawSegment( pB, b2Add( pB, rhi ), c3, draw->context ); - b2Vec2 ref = ( b2Vec2 ){ L * cosf( joint->referenceAngle ), L * sinf( joint->referenceAngle ) }; + b2Rot rotRef = b2MakeRot( joint->referenceAngle ); + b2Vec2 ref = ( b2Vec2 ){ L * rotRef.c, L * rotRef.s }; draw->DrawSegment( pB, b2Add( pB, ref ), b2_colorBlue, draw->context ); } diff --git a/src/solver.c b/src/solver.c index e5e087ad7..de2be855e 100644 --- a/src/solver.c +++ b/src/solver.c @@ -19,6 +19,7 @@ #include #include #include +#include #if defined(B2_CPU_ARM) static inline void b2Pause (void) diff --git a/src/timer.c b/src/timer.c index df2420ab3..8e49990e6 100644 --- a/src/timer.c +++ b/src/timer.c @@ -5,11 +5,11 @@ #if defined( _WIN32 ) - #ifndef WIN32_LEAN_AND_MEAN - #define WIN32_LEAN_AND_MEAN - #endif +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif - #include +#include static double s_invFrequency = 0.0; @@ -76,9 +76,9 @@ void b2Yield() #elif defined( __linux__ ) || defined( __APPLE__ ) - #include - #include - #include +#include +#include +#include b2Timer b2CreateTimer( void ) { @@ -185,3 +185,14 @@ void b2Yield() } #endif + +uint32_t b2Hash( uint32_t hash, const uint8_t* data, int count ) +{ + uint32_t result = hash; + for ( size_t i = 0; i < count; i++ ) + { + result = ( result << 5 ) + result + data[i]; + } + + return result; +} diff --git a/src/types.c b/src/types.c index e2354c739..f7c230403 100644 --- a/src/types.c +++ b/src/types.c @@ -20,7 +20,7 @@ b2WorldDef b2DefaultWorldDef( void ) // 400 meters per second, faster than the speed of sound def.maximumLinearVelocity = 400.0f * b2_lengthUnitsPerMeter; def.enableSleep = true; - def.enableContinous = true; + def.enableContinuous = true; def.internalValue = B2_SECRET_COOKIE; return def; } @@ -42,13 +42,13 @@ b2BodyDef b2DefaultBodyDef( void ) b2Filter b2DefaultFilter( void ) { - b2Filter filter = { 0x00000001, 0xFFFFFFFF, 0 }; + b2Filter filter = { 0x0001ULL, UINT64_MAX, 0 }; return filter; } b2QueryFilter b2DefaultQueryFilter( void ) { - b2QueryFilter filter = { 0x00000001, 0xFFFFFFFF }; + b2QueryFilter filter = { 0x0001ULL, UINT64_MAX }; return filter; } @@ -72,3 +72,103 @@ b2ChainDef b2DefaultChainDef( void ) def.internalValue = B2_SECRET_COOKIE; return def; } + +static void b2EmptyDrawPolygon( const b2Vec2* vertices, int vertexCount, b2HexColor color, void* context ) +{ + B2_MAYBE_UNUSED( vertices ); + B2_MAYBE_UNUSED( vertexCount ); + B2_MAYBE_UNUSED( color ); + B2_MAYBE_UNUSED( context ); +} + +static void b2EmptyDrawSolidPolygon( b2Transform transform, const b2Vec2* vertices, int vertexCount, float radius, b2HexColor color, + void* context ) +{ + B2_MAYBE_UNUSED( transform ); + B2_MAYBE_UNUSED( vertices ); + B2_MAYBE_UNUSED( vertexCount ); + B2_MAYBE_UNUSED( radius ); + B2_MAYBE_UNUSED( color ); + B2_MAYBE_UNUSED( context ); +} + +static void b2EmptyDrawCircle( b2Vec2 center, float radius, b2HexColor color, void* context ) +{ + B2_MAYBE_UNUSED( center ); + B2_MAYBE_UNUSED( radius ); + B2_MAYBE_UNUSED( color ); + B2_MAYBE_UNUSED( context ); +} + +static void b2EmptyDrawSolidCircle( b2Transform transform, float radius, b2HexColor color, void* context ) +{ + B2_MAYBE_UNUSED( transform ); + B2_MAYBE_UNUSED( radius ); + B2_MAYBE_UNUSED( color ); + B2_MAYBE_UNUSED( context ); +} + +static void b2EmptyDrawCapsule( b2Vec2 p1, b2Vec2 p2, float radius, b2HexColor color, void* context ) +{ + B2_MAYBE_UNUSED( p1 ); + B2_MAYBE_UNUSED( p2 ); + B2_MAYBE_UNUSED( radius ); + B2_MAYBE_UNUSED( color ); + B2_MAYBE_UNUSED( context ); +} + +static void b2EmptyDrawSolidCapsule( b2Vec2 p1, b2Vec2 p2, float radius, b2HexColor color, void* context ) +{ + B2_MAYBE_UNUSED( p1 ); + B2_MAYBE_UNUSED( p2 ); + B2_MAYBE_UNUSED( radius ); + B2_MAYBE_UNUSED( color ); + B2_MAYBE_UNUSED( context ); +} + +static void b2EmptyDrawSegment( b2Vec2 p1, b2Vec2 p2, b2HexColor color, void* context ) +{ + B2_MAYBE_UNUSED( p1 ); + B2_MAYBE_UNUSED( p2 ); + B2_MAYBE_UNUSED( color ); + B2_MAYBE_UNUSED( context ); +} + +static void b2EmptyDrawTransform( b2Transform transform, void* context ) +{ + B2_MAYBE_UNUSED( transform ); + B2_MAYBE_UNUSED( context ); +} + +static void b2EmptyDrawPoint( b2Vec2 p, float size, b2HexColor color, void* context ) +{ + B2_MAYBE_UNUSED( p ); + B2_MAYBE_UNUSED( size ); + B2_MAYBE_UNUSED( color ); + B2_MAYBE_UNUSED( context ); +} + +static void b2EmptyDrawString( b2Vec2 p, const char* s, void* context ) +{ + B2_MAYBE_UNUSED( p ); + B2_MAYBE_UNUSED( s ); + B2_MAYBE_UNUSED( context ); +} + +b2DebugDraw b2DefaultDebugDraw(void) +{ + b2DebugDraw draw = { 0 }; + + // These allow the user to skip some implementations and not hit null exceptions. + draw.DrawPolygon = b2EmptyDrawPolygon; + draw.DrawSolidPolygon = b2EmptyDrawSolidPolygon; + draw.DrawCircle = b2EmptyDrawCircle; + draw.DrawSolidCircle = b2EmptyDrawSolidCircle; + draw.DrawCapsule = b2EmptyDrawCapsule; + draw.DrawSolidCapsule = b2EmptyDrawSolidCapsule; + draw.DrawSegment = b2EmptyDrawSegment; + draw.DrawTransform = b2EmptyDrawTransform; + draw.DrawPoint = b2EmptyDrawPoint; + draw.DrawString = b2EmptyDrawString; + return draw; +} diff --git a/src/world.c b/src/world.c index bd1f68a3f..d84728fd2 100644 --- a/src/world.c +++ b/src/world.c @@ -174,7 +174,7 @@ b2WorldId b2CreateWorld( const b2WorldDef* def ) world->enableSleep = def->enableSleep; world->locked = false; world->enableWarmStarting = true; - world->enableContinuous = def->enableContinous; + world->enableContinuous = def->enableContinuous; world->userTreeTask = NULL; if ( def->workerCount > 0 && def->enqueueTask != NULL && def->finishTask != NULL ) diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 7f7acdbef..0f20a5607 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -22,8 +22,11 @@ set_target_properties(test PROPERTIES # Special access to Box2D internals for testing target_include_directories(test PRIVATE ${CMAKE_SOURCE_DIR}/src) -if(MSVC) +if (MSVC) target_compile_options(test PRIVATE /experimental:c11atomics) +elseif (APPLE) + # determinism settings + target_compile_options(test PRIVATE -ffp-model=precise -ffp-contract=off) endif() target_link_libraries(test PRIVATE box2d enkiTS) diff --git a/test/test_determinism.c b/test/test_determinism.c index 50b3bef61..b5cfe4f83 100644 --- a/test/test_determinism.c +++ b/test/test_determinism.c @@ -4,11 +4,13 @@ #include "TaskScheduler_c.h" #include "test_macros.h" +#include "box2d/base.h" #include "box2d/box2d.h" #include "box2d/math_functions.h" #include "box2d/types.h" #include +#include #ifdef BOX2D_PROFILE #include @@ -38,7 +40,7 @@ enkiTaskSet* tasks[e_maxTasks]; TaskData taskData[e_maxTasks]; int taskCount; -void ExecuteRangeTask( uint32_t start, uint32_t end, uint32_t threadIndex, void* context ) +static void ExecuteRangeTask( uint32_t start, uint32_t end, uint32_t threadIndex, void* context ) { TaskData* data = context; data->box2dTask( start, end, threadIndex, data->box2dContext ); @@ -84,7 +86,7 @@ static void FinishTask( void* userTask, void* userContext ) enkiWaitForTaskSet( scheduler, task ); } -void TiltedStacks( int testIndex, int workerCount ) +static void TiltedStacks( int testIndex, int workerCount ) { scheduler = enkiNewTaskScheduler(); struct enkiTaskSchedulerConfig config = enkiGetTaskSchedulerConfig( scheduler ); @@ -96,12 +98,7 @@ void TiltedStacks( int testIndex, int workerCount ) tasks[i] = enkiCreateTaskSet( scheduler, ExecuteRangeTask ); } - // Define the gravity vector. - b2Vec2 gravity = { 0.0f, -10.0f }; - - // Construct a world object, which will hold and simulate the rigid bodies. b2WorldDef worldDef = b2DefaultWorldDef(); - worldDef.gravity = gravity; worldDef.enqueueTask = EnqueueTask; worldDef.finishTask = FinishTask; worldDef.workerCount = workerCount; @@ -176,7 +173,7 @@ void TiltedStacks( int testIndex, int workerCount ) } // Test multithreaded determinism. -int DeterminismTest( void ) +static int MultithreadingTest( void ) { // Test 1 : 4 threads TiltedStacks( 0, 4 ); @@ -200,3 +197,155 @@ int DeterminismTest( void ) return 0; } + +// Test cross platform determinism based on the FallingHinges sample. +static int CrossPlatformTest(void) +{ + b2WorldDef worldDef = b2DefaultWorldDef(); + b2WorldId worldId = b2CreateWorld( &worldDef ); + + { + b2BodyDef bodyDef = b2DefaultBodyDef(); + bodyDef.position = (b2Vec2){ 0.0f, -1.0f }; + b2BodyId groundId = b2CreateBody( worldId, &bodyDef ); + + b2Polygon box = b2MakeBox( 20.0f, 1.0f ); + b2ShapeDef shapeDef = b2DefaultShapeDef(); + b2CreatePolygonShape( groundId, &shapeDef, &box ); + } + + int columnCount = 4; + int rowCount = 30; + int bodyCount = rowCount * columnCount; + + b2BodyId* bodies = calloc( bodyCount, sizeof( b2BodyId ) ); + + float h = 0.25f; + float r = 0.1f * h; + b2Polygon box = b2MakeRoundedBox( h - r, h - r, r ); + + b2ShapeDef shapeDef = b2DefaultShapeDef(); + shapeDef.friction = 0.3f; + + float offset = 0.4f * h; + float dx = 10.0f * h; + float xroot = -0.5f * dx * ( columnCount - 1.0f ); + + b2RevoluteJointDef jointDef = b2DefaultRevoluteJointDef(); + jointDef.enableLimit = true; + jointDef.lowerAngle = -0.1f * b2_pi; + jointDef.upperAngle = 0.2f * b2_pi; + jointDef.enableSpring = true; + jointDef.hertz = 0.5f; + jointDef.dampingRatio = 0.5f; + jointDef.localAnchorA = (b2Vec2){ h, h }; + jointDef.localAnchorB = (b2Vec2){ offset, -h }; + jointDef.drawSize = 0.1f; + + int bodyIndex = 0; + + for ( int j = 0; j < columnCount; ++j ) + { + float x = xroot + j * dx; + + b2BodyId prevBodyId = b2_nullBodyId; + + for ( int i = 0; i < rowCount; ++i ) + { + b2BodyDef bodyDef = b2DefaultBodyDef(); + bodyDef.type = b2_dynamicBody; + + bodyDef.position.x = x + offset * i; + bodyDef.position.y = h + 2.0f * h * i; + + // this tests the deterministic cosine and sine functions + bodyDef.rotation = b2MakeRot( 0.1f * i - 1.0f ); + + b2BodyId bodyId = b2CreateBody( worldId, &bodyDef ); + + if ( ( i & 1 ) == 0 ) + { + prevBodyId = bodyId; + } + else + { + jointDef.bodyIdA = prevBodyId; + jointDef.bodyIdB = bodyId; + b2CreateRevoluteJoint( worldId, &jointDef ); + prevBodyId = b2_nullBodyId; + } + + b2CreatePolygonShape( bodyId, &shapeDef, &box ); + + assert( bodyIndex < bodyCount ); + bodies[bodyIndex] = bodyId; + + bodyIndex += 1; + } + } + + assert( bodyIndex == bodyCount ); + + uint32_t hash = 0; + int sleepStep = -1; + float timeStep = 1.0f / 60.0f; + int subStepCount = 4; + + int stepCount = 0; + int maxSteps = 500; + while ( stepCount < maxSteps ) + { + b2World_Step( worldId, timeStep, subStepCount ); + TracyCFrameMark; + + if ( hash == 0 ) + { + bool sleeping = true; + for ( int i = 0; i < bodyCount; ++i ) + { + if ( b2Body_IsAwake( bodies[i] ) == true ) + { + sleeping = false; + break; + } + } + + if ( sleeping == true ) + { + hash = B2_HASH_INIT; + for ( int i = 0; i < bodyCount; ++i ) + { + b2Transform xf = b2Body_GetTransform( bodies[i] ); + hash = b2Hash( hash, (uint8_t*)( &xf ), sizeof( b2Transform ) ); + } + + sleepStep = stepCount; + printf( "step = %d, hash = 0x%08x\n", sleepStep, hash ); + + break; + } + } + + stepCount += 1; + } + + ENSURE( stepCount < maxSteps ); + + // sleep step = 310, hash = 0x5e70e5fe + ENSURE( sleepStep == 310 ); + ENSURE( hash == 0x5e70e5fe ); + + free( bodies ); + + b2DestroyWorld( worldId ); + + return 0; +} + +int DeterminismTest( void ) +{ + RUN_SUBTEST( MultithreadingTest ); + RUN_SUBTEST( CrossPlatformTest ); + + return 0; +} diff --git a/test/test_math.c b/test/test_math.c index ed545f663..fb1460a6a 100644 --- a/test/test_math.c +++ b/test/test_math.c @@ -6,9 +6,35 @@ #include "box2d/math_functions.h" #include +#include int MathTest( void ) { + for (float x = -10.0f * b2_pi; x < 10.0f * b2_pi; x += 0.01f ) + { + b2Rot r = b2MakeRot( x ); + float c = cosf( x ); + float s = sinf( x ); + + // The cosine and sine approximations are accurate to about 0.1 degrees (0.002 radians) + //printf( "%g %g\n", r.c - c, r.s - s ); + ENSURE_SMALL( r.c - c, 0.002f ); + ENSURE_SMALL( r.s - s, 0.002f ); + + float xn = b2UnwindLargeAngle( x ); + float a = b2Atan2( s, c ); + float diff = b2AbsFloat( a - xn ); + + // The two results can be off by 360 degrees (-pi and pi) + if (diff > b2_pi) + { + diff -= 2.0f * b2_pi; + } + + // The approximate atan2 is quite accurate + ENSURE_SMALL( diff, 1e-5f ); + } + b2Vec2 zero = b2Vec2_zero; b2Vec2 one = { 1.0f, 1.0f }; b2Vec2 two = { 2.0f, 2.0f }; diff --git a/test/test_world.c b/test/test_world.c index deeaea300..ffb7473d7 100644 --- a/test/test_world.c +++ b/test/test_world.c @@ -198,8 +198,106 @@ static int TestIsValid( void ) return 0; } +int TestForAmy( void ) +{ + { + b2WorldDef worldDef = b2DefaultWorldDef(); + b2WorldId world_id = b2CreateWorld( &worldDef ); + + b2BodyDef body_def = b2DefaultBodyDef(); + + body_def.type = b2_staticBody; + body_def.position = ( b2Vec2 ){ 0., 0. }; + b2BodyId body_id = b2CreateBody( world_id, &body_def ); + b2Polygon polygon = b2MakeBox( 1., 1. ); + b2ShapeDef shape_def = b2DefaultShapeDef(); + b2CreatePolygonShape( body_id, &shape_def, &polygon ); + + b2BodyDef simulon_body_def = b2DefaultBodyDef(); + + simulon_body_def.position = ( b2Vec2 ){ 0., -7.5 }; + simulon_body_def.type = b2_dynamicBody; + + b2BodyId simulon_body_id = b2CreateBody( world_id, &simulon_body_def ); + b2Circle ball = { { 0.0, 0.35 }, 0.5 }; + + b2ShapeDef simulon_shape_def = b2DefaultShapeDef(); + b2CreateCircleShape( simulon_body_id, &simulon_shape_def, &ball ); + + b2Polygon the_box = b2MakeRoundedBox( 0.1, 0.1, 0.01 ); + b2CreatePolygonShape( simulon_body_id, &simulon_shape_def, &the_box ); + b2BodyDef head_body_def = b2DefaultBodyDef(); + head_body_def.position = ( b2Vec2 ){ 0., 6. }; + head_body_def.type = b2_dynamicBody; + b2BodyId head_body_id = b2CreateBody( world_id, &head_body_def ); + b2RevoluteJointDef joint_def5 = b2DefaultRevoluteJointDef(); + joint_def5.bodyIdA = simulon_body_id; + joint_def5.bodyIdB = head_body_id; + joint_def5.localAnchorA = ( b2Vec2 ){ 0.0, 0.8 }; + joint_def5.localAnchorB = ( b2Vec2 ){ 0.0, -0.17 / 2.0 }; + + b2JointId revolute_joint_id = b2CreateRevoluteJoint( world_id, &joint_def5 ); + b2DistanceJointDef joint_def6 = b2DefaultDistanceJointDef(); + joint_def6.bodyIdA = simulon_body_id; + joint_def6.bodyIdB = head_body_id; + joint_def6.localAnchorA = ( b2Vec2 ){ 0.0, 1.7 }; + joint_def6.localAnchorB = ( b2Vec2 ){ 0.0, 0.8 }; + joint_def6.length = 0.005; + joint_def6.hertz = 1.; + b2CreateDistanceJoint( world_id, &joint_def6 ); + + b2DestroyBody( simulon_body_id ); + + b2World_Step( world_id, 1. / 60., 4 ); + + b2DestroyWorld( world_id ); + } + + { + b2WorldDef worldDef = b2DefaultWorldDef(); + b2WorldId world_id = b2CreateWorld( &worldDef ); + + b2BodyDef ground_body_def = b2DefaultBodyDef(); + ground_body_def.type = b2_staticBody; + b2BodyId ground_body_id = b2CreateBody( world_id, &ground_body_def ); + + b2BodyDef box_body_def = b2DefaultBodyDef(); + box_body_def.type = b2_dynamicBody; + box_body_def.position = ( b2Vec2 ){ 0.0, 0.0 }; + b2BodyId box_body_id = b2CreateBody( world_id, &box_body_def ); + b2Polygon polygon = b2MakeBox( 1., 1. ); + b2ShapeDef shape_def = b2DefaultShapeDef(); + b2ShapeId box_shape = b2CreatePolygonShape( box_body_id, &shape_def, &polygon ); + + b2DistanceJointDef distance_joint_def = b2DefaultDistanceJointDef(); + distance_joint_def.hertz = 1.; + distance_joint_def.dampingRatio = 0.1; + distance_joint_def.bodyIdA = ground_body_id; + distance_joint_def.bodyIdB = box_body_id; + distance_joint_def.minLength = 0.005; + distance_joint_def.enableSpring = true; + distance_joint_def.enableLimit = false; + distance_joint_def.collideConnected = false; + distance_joint_def.length = 0.005; + b2Body_SetTransform( ground_body_id, ( b2Vec2 ){ 0.0, 0.0 }, ( b2Rot ){ 1., 0. } ); + distance_joint_def.localAnchorA = ( b2Vec2 ){ 0.0, 0.0 }; + distance_joint_def.localAnchorB = ( b2Vec2 ){ 0.0, 0.0 }; + b2JointId distance_joint_id = b2CreateDistanceJoint( world_id, &distance_joint_def ); + + b2Body_SetType( box_body_id, b2_staticBody ); + b2World_Step( world_id, 1. / 60., 4 ); + + b2DestroyJoint( distance_joint_id ); + + b2DestroyWorld( world_id ); + } + + return 0; +} + int WorldTest( void ) { + RUN_SUBTEST( TestForAmy ); RUN_SUBTEST( HelloWorld ); RUN_SUBTEST( EmptyWorld ); RUN_SUBTEST( DestroyAllBodiesWorld );