From accbc88bd27788c83ebd0656a41ac1d7d767d467 Mon Sep 17 00:00:00 2001 From: ikpil Date: Wed, 12 Aug 2026 01:44:56 +0900 Subject: [PATCH] fix: restore the Equals/GetHashCode contract on B2Vec2 Moving operator== to an IEEE compare was the right call, it matches C. But Equals just forwarded to the operator, so a NaN vector stopped being equal to itself and stopped agreeing with GetHashCode. That breaks Dictionary. Keep the operator on IEEE semantics and compare bitwise in Equals. While here: - b2World_Draw takes the debug draw by in. C passes b2DebugDraw*, and the struct has 27 fields, so it was copied every frame. - b2GrowBitSet guards its copy the way b2CreateBitSet already guards the fill. Growing a bit set created with zero capacity threw, because b2Alloc hands back null there. - b2ClearBit takes an int like the other three, so the call sites drop their casts. - b2InternalAssert is public. It is B2_API upstream. --- src/Box2D.NET/B2BitSets.cs | 19 ++++++++++++++----- src/Box2D.NET/B2ConstraintGraphs.cs | 8 ++++---- src/Box2D.NET/B2Diagnostics.cs | 2 +- src/Box2D.NET/B2SolverSets.cs | 8 ++++---- src/Box2D.NET/B2Vec2.cs | 4 +++- src/Box2D.NET/B2Worlds.cs | 2 +- 6 files changed, 27 insertions(+), 16 deletions(-) diff --git a/src/Box2D.NET/B2BitSets.cs b/src/Box2D.NET/B2BitSets.cs index 3446420f..b722a32c 100644 --- a/src/Box2D.NET/B2BitSets.cs +++ b/src/Box2D.NET/B2BitSets.cs @@ -42,15 +42,20 @@ public static void b2SetBitGrow(ref B2BitSet bitSet, int bitIndex) } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void b2ClearBit(ref B2BitSet bitSet, uint bitIndex) + public static void b2ClearBit(ref B2BitSet bitSet, int bitIndex) { - uint blockIndex = bitIndex / 64; + if (bitIndex < 0) + { + return; + } + + int blockIndex = bitIndex / 64; if (blockIndex >= bitSet.blockCount) { return; } - bitSet.bits[blockIndex] &= ~((ulong)1 << (int)(bitIndex % 64)); + bitSet.bits[blockIndex] &= ~((ulong)1 << (bitIndex % 64)); } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -137,9 +142,13 @@ public static void b2GrowBitSet(ref B2BitSet bitSet, int blockCount) ulong[] newBits = b2Alloc(bitSet.blockCapacity); //memset( newBits, 0, bitSet->blockCapacity * sizeof( ulong ) ); Array.Fill(newBits, 0UL, 0, bitSet.blockCapacity); - B2_ASSERT(bitSet.bits != null); //memcpy( newBits, bitSet->bits, oldCapacity * sizeof( ulong ) ); - Array.Copy(bitSet.bits, newBits, oldCapacity); + // b2Alloc returns null for a zero capacity, and C's memcpy from NULL with a zero + // length is harmless, so guard rather than fault on the first grow. + if (oldCapacity > 0) + { + Array.Copy(bitSet.bits, newBits, oldCapacity); + } b2Free(bitSet.bits, oldCapacity); bitSet.bits = newBits; } diff --git a/src/Box2D.NET/B2ConstraintGraphs.cs b/src/Box2D.NET/B2ConstraintGraphs.cs index 457a8393..23c7c01c 100644 --- a/src/Box2D.NET/B2ConstraintGraphs.cs +++ b/src/Box2D.NET/B2ConstraintGraphs.cs @@ -207,8 +207,8 @@ internal static void b2RemoveContactFromGraph(B2World world, int bodyIdA, int bo if (colorIndex != B2_OVERFLOW_INDEX) { // This might clear a bit for a kinematic or static body, but this has no effect - b2ClearBit(ref color.bodySet, (uint)bodyIdA); - b2ClearBit(ref color.bodySet, (uint)bodyIdB); + b2ClearBit(ref color.bodySet, bodyIdA); + b2ClearBit(ref color.bodySet, bodyIdB); } int movedIndex = b2Array_RemoveSwap(ref color.contactSims, localIndex); @@ -324,8 +324,8 @@ internal static void b2RemoveJointFromGraph(B2World world, int bodyIdA, int body if (colorIndex != B2_OVERFLOW_INDEX) { // May clear static bodies, no effect - b2ClearBit(ref color.bodySet, (uint)bodyIdA); - b2ClearBit(ref color.bodySet, (uint)bodyIdB); + b2ClearBit(ref color.bodySet, bodyIdA); + b2ClearBit(ref color.bodySet, bodyIdB); } int movedIndex = b2Array_RemoveSwap(ref color.jointSims, localIndex); diff --git a/src/Box2D.NET/B2Diagnostics.cs b/src/Box2D.NET/B2Diagnostics.cs index 8d2805ea..98792a39 100644 --- a/src/Box2D.NET/B2Diagnostics.cs +++ b/src/Box2D.NET/B2Diagnostics.cs @@ -97,7 +97,7 @@ public static void b2SetAssertFcn(b2AssertFcn assertFcn) b2AssertHandler = assertFcn; } - internal static int b2InternalAssert(string condition, string fileName, int lineNumber) + public static int b2InternalAssert(string condition, string fileName, int lineNumber) { return b2AssertHandler(condition, fileName, lineNumber); } diff --git a/src/Box2D.NET/B2SolverSets.cs b/src/Box2D.NET/B2SolverSets.cs index 1501304c..016d9df1 100644 --- a/src/Box2D.NET/B2SolverSets.cs +++ b/src/Box2D.NET/B2SolverSets.cs @@ -330,8 +330,8 @@ internal static void b2TrySleepIsland(B2World world, int islandId) if (colorIndex != B2_OVERFLOW_INDEX) { // might clear a bit for a static body, but this has no effect - b2ClearBit(ref color.bodySet, (uint)contact.edges[0].bodyId); - b2ClearBit(ref color.bodySet, (uint)contact.edges[1].bodyId); + b2ClearBit(ref color.bodySet, contact.edges[0].bodyId); + b2ClearBit(ref color.bodySet, contact.edges[1].bodyId); } int localIndex = contact.localIndex; @@ -380,8 +380,8 @@ internal static void b2TrySleepIsland(B2World world, int islandId) if (colorIndex != B2_OVERFLOW_INDEX) { // might clear a bit for a static body, but this has no effect - b2ClearBit(ref color.bodySet, (uint)joint.edges[0].bodyId); - b2ClearBit(ref color.bodySet, (uint)joint.edges[1].bodyId); + b2ClearBit(ref color.bodySet, joint.edges[0].bodyId); + b2ClearBit(ref color.bodySet, joint.edges[1].bodyId); } int sleepJointIndex = sleepSet.jointSims.count; diff --git a/src/Box2D.NET/B2Vec2.cs b/src/Box2D.NET/B2Vec2.cs index d7a9f363..7eda33a5 100644 --- a/src/Box2D.NET/B2Vec2.cs +++ b/src/Box2D.NET/B2Vec2.cs @@ -78,9 +78,11 @@ public B2Vec2(float x, float y) return !(a == b); } + // Not "this == other". The operator follows C and uses IEEE compare, where NaN != NaN. + // Equals must stay reflexive and agree with GetHashCode, so it compares bitwise. public bool Equals(B2Vec2 other) { - return this == other; + return X.Equals(other.X) && Y.Equals(other.Y); } public override bool Equals(object obj) diff --git a/src/Box2D.NET/B2Worlds.cs b/src/Box2D.NET/B2Worlds.cs index e7bd31e5..b392e44a 100644 --- a/src/Box2D.NET/B2Worlds.cs +++ b/src/Box2D.NET/B2Worlds.cs @@ -1152,7 +1152,7 @@ internal static bool DrawQueryCallback(int proxyId, ulong userData, ref B2DrawCo // 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. /// Call this to draw shapes and other debug draw data - public static void b2World_Draw(B2WorldId worldId, B2DebugDraw draw) + public static void b2World_Draw(B2WorldId worldId, in B2DebugDraw draw) { B2World world = b2GetWorldFromId(worldId); B2_ASSERT(world.locked == false);