Skip to content

Commit e6384fc

Browse files
committed
[upstream] Optimizations (erincatto/box2d#1050)
Improved performance, up to 20% gain in stacking scenarios. Behavior change: contact solver bias stage no longer applies friction (almost free perf gain) Behavior change: b2World_Step still performs all operations, including collision when timeStep == 0. b2WorldDef now has b2Capacity that lets you pre-size some arrays to reduce allocations. Optimized hit events. These are basically free now. Cleaned up and optimized solver scheduling. Converted to new dynamic array type. Added threading logic to prevent theoretical solver deadlock if the user task system stalls worker 0. Added Compounds benchmark.
1 parent a157761 commit e6384fc

14 files changed

Lines changed: 211 additions & 55 deletions

File tree

src/Box2D.NET.Samples/Samples/Sample.cs

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -175,8 +175,8 @@ public virtual void UpdateGui()
175175
aveProfile.pairs = scale * m_totalProfile.pairs;
176176
aveProfile.collide = scale * m_totalProfile.collide;
177177
aveProfile.solve = scale * m_totalProfile.solve;
178-
aveProfile.prepareStages = scale * m_totalProfile.prepareStages;
179-
aveProfile.solveConstraints = scale * m_totalProfile.solveConstraints;
178+
aveProfile.solverSetup = scale * m_totalProfile.solverSetup;
179+
aveProfile.constraints = scale * m_totalProfile.constraints;
180180
aveProfile.prepareConstraints = scale * m_totalProfile.prepareConstraints;
181181
aveProfile.integrateVelocities = scale * m_totalProfile.integrateVelocities;
182182
aveProfile.warmStart = scale * m_totalProfile.warmStart;
@@ -200,8 +200,8 @@ public virtual void UpdateGui()
200200
DrawTextLine($"pairs [ave] (max) = {p.pairs,5:F2} [{aveProfile.pairs,6:F2}] ({m_maxProfile.pairs,6:F2})");
201201
DrawTextLine($"collide [ave] (max) = {p.collide,5:F2} [{aveProfile.collide,6:F2}] ({m_maxProfile.collide,6:F2})");
202202
DrawTextLine($"solve [ave] (max) = {p.solve,5:F2} [{aveProfile.solve,6:F2}] ({m_maxProfile.solve,6:F2})");
203-
DrawTextLine($"> prepare tasks [ave] (max) = {p.prepareStages,5:F2} [{aveProfile.prepareStages,6:F2}] ({m_maxProfile.prepareStages,6:F2})");
204-
DrawTextLine($"> solve constraints [ave] (max) = {p.solveConstraints,5:F2} [{aveProfile.solveConstraints,6:F2}] ({m_maxProfile.solveConstraints,6:F2})");
203+
DrawTextLine($"> solver setup [ave] (max) = {p.solverSetup,5:F2} [{aveProfile.solverSetup,6:F2}] ({m_maxProfile.solverSetup,6:F2})");
204+
DrawTextLine($"> constraints [ave] (max) = {p.constraints,5:F2} [{aveProfile.constraints,6:F2}] ({m_maxProfile.constraints,6:F2})");
205205
DrawTextLine($">> prepare constraints [ave] (max) = {p.prepareConstraints,5:F2} [{aveProfile.prepareConstraints,6:F2}] ({m_maxProfile.prepareConstraints,6:F2})");
206206
DrawTextLine($">> integrate velocities [ave] (max) = {p.integrateVelocities,5:F2} [{aveProfile.integrateVelocities,6:F2}] ({m_maxProfile.integrateVelocities,6:F2})");
207207
DrawTextLine($">> warm start [ave] (max) = {p.warmStart,5:F2} [{aveProfile.warmStart,6:F2}] ({m_maxProfile.warmStart,6:F2})");
@@ -538,8 +538,8 @@ public virtual void Step()
538538
m_maxProfile.pairs = b2MaxFloat(m_maxProfile.pairs, p.pairs);
539539
m_maxProfile.collide = b2MaxFloat(m_maxProfile.collide, p.collide);
540540
m_maxProfile.solve = b2MaxFloat(m_maxProfile.solve, p.solve);
541-
m_maxProfile.prepareStages = b2MaxFloat(m_maxProfile.prepareStages, p.prepareStages);
542-
m_maxProfile.solveConstraints = b2MaxFloat(m_maxProfile.solveConstraints, p.solveConstraints);
541+
m_maxProfile.solverSetup = b2MaxFloat(m_maxProfile.solverSetup, p.solverSetup);
542+
m_maxProfile.constraints = b2MaxFloat(m_maxProfile.constraints, p.constraints);
543543
m_maxProfile.prepareConstraints = b2MaxFloat(m_maxProfile.prepareConstraints, p.prepareConstraints);
544544
m_maxProfile.integrateVelocities = b2MaxFloat(m_maxProfile.integrateVelocities, p.integrateVelocities);
545545
m_maxProfile.warmStart = b2MaxFloat(m_maxProfile.warmStart, p.warmStart);
@@ -561,8 +561,8 @@ public virtual void Step()
561561
m_totalProfile.pairs += p.pairs;
562562
m_totalProfile.collide += p.collide;
563563
m_totalProfile.solve += p.solve;
564-
m_totalProfile.prepareStages += p.prepareStages;
565-
m_totalProfile.solveConstraints += p.solveConstraints;
564+
m_totalProfile.solverSetup += p.solverSetup;
565+
m_totalProfile.constraints += p.constraints;
566566
m_totalProfile.prepareConstraints += p.prepareConstraints;
567567
m_totalProfile.integrateVelocities += p.integrateVelocities;
568568
m_totalProfile.warmStart += p.warmStart;

src/Box2D.NET/B2BroadPhases.cs

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ public static void b2BufferMove(B2BroadPhase bp, int queryProxy)
6666

6767

6868
// #include <stdio.h>
69-
public static void b2CreateBroadPhase(ref B2BroadPhase bp)
69+
public static void b2CreateBroadPhase(ref B2BroadPhase bp, in B2Capacity capacity)
7070
{
7171
B2_ASSERT((int)B2BodyType.b2_bodyTypeCount == 3, "must be three body types");
7272

@@ -77,18 +77,27 @@ public static void b2CreateBroadPhase(ref B2BroadPhase bp)
7777
// }
7878
bp = new B2BroadPhase();
7979
bp.trees = new B2DynamicTree[(int)B2BodyType.b2_bodyTypeCount];
80-
bp.moveSet = b2CreateSet(16);
81-
bp.moveArray = b2Array_Create<int>(16);
80+
bp.moveSet = b2CreateSet(b2MaxInt(16, 2 * capacity.dynamicShapeCount));
81+
bp.moveArray = b2Array_Create<int>(b2MaxInt(16, capacity.dynamicShapeCount));
8282
bp.moveResults = null;
8383
bp.movePairs = null;
8484
bp.movePairCapacity = 0;
8585
b2AtomicStoreInt(ref bp.movePairIndex, 0);
86-
bp.pairSet = b2CreateSet(32);
86+
bp.pairSet = b2CreateSet(b2MaxInt(32, 2 * capacity.contactCount));
8787

88-
for (int i = 0; i < (int)B2BodyType.b2_bodyTypeCount; ++i)
89-
{
90-
bp.trees[i] = b2DynamicTree_Create();
91-
}
88+
int staticCapacity = b2MaxInt(16, capacity.staticShapeCount);
89+
bp.trees[(int)B2BodyType.b2_staticBody] = b2DynamicTree_Create(staticCapacity);
90+
91+
int kinematicCapacity = 16;
92+
bp.trees[(int)B2BodyType.b2_kinematicBody] = b2DynamicTree_Create(kinematicCapacity);
93+
94+
int dynamicCapacity = b2MaxInt(16, capacity.dynamicShapeCount);
95+
bp.trees[(int)B2BodyType.b2_dynamicBody] = b2DynamicTree_Create(dynamicCapacity);
96+
}
97+
98+
public static void b2CreateBroadPhase(ref B2BroadPhase bp)
99+
{
100+
b2CreateBroadPhase(ref bp, new B2Capacity());
92101
}
93102

94103
public static void b2DestroyBroadPhase(B2BroadPhase bp)
@@ -410,7 +419,8 @@ public static void b2UpdateTreesTask(object context)
410419
b2TracyCZoneNC(B2TracyCZone.tree_task, "Rebuild BVH", B2HexColor.b2_colorFireBrick, true);
411420

412421
B2World world = (B2World)context;
413-
b2BroadPhase_RebuildTrees(world.broadPhase);
422+
b2DynamicTree_Rebuild(world.broadPhase.trees[(int)B2BodyType.b2_dynamicBody], false);
423+
b2DynamicTree_Rebuild(world.broadPhase.trees[(int)B2BodyType.b2_kinematicBody], false);
414424

415425
b2TracyCZoneEnd(B2TracyCZone.tree_task);
416426
}

src/Box2D.NET/B2Capacity.cs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
// SPDX-FileCopyrightText: 2025 Erin Catto
2+
// SPDX-FileCopyrightText: 2025 Ikpil Choi(ikpil@naver.com)
3+
// SPDX-License-Identifier: MIT
4+
5+
namespace Box2D.NET
6+
{
7+
/// Optional world capacities that can be used to avoid run-time allocations.
8+
/// @see b2World_GetMaxCapacity
9+
/// @ingroup world
10+
public struct B2Capacity
11+
{
12+
/// Number of expected static shapes.
13+
public int staticShapeCount;
14+
15+
/// Number of expected dynamic and kinematic shapes.
16+
public int dynamicShapeCount;
17+
18+
/// Number of expected static bodies.
19+
public int staticBodyCount;
20+
21+
/// Number of expected dynamic and kinematic bodies.
22+
public int dynamicBodyCount;
23+
24+
/// Number of expected contacts.
25+
public int contactCount;
26+
}
27+
}

src/Box2D.NET/B2ConstraintGraphs.cs

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ public static class B2ConstraintGraphs
3636
public const int B2_DYNAMIC_COLOR_COUNT = (B2_GRAPH_COLOR_COUNT - 4);
3737

3838

39-
public static void b2CreateGraph(ref B2ConstraintGraph graph, int bodyCapacity)
39+
public static void b2CreateGraph(ref B2ConstraintGraph graph, in B2Capacity capacity)
4040
{
4141
B2_ASSERT(B2_GRAPH_COLOR_COUNT >= 2, "must have at least two constraint graph colors");
4242
B2_ASSERT(B2_OVERFLOW_INDEX == B2_GRAPH_COLOR_COUNT - 1, "bad over flow index");
@@ -45,7 +45,7 @@ public static void b2CreateGraph(ref B2ConstraintGraph graph, int bodyCapacity)
4545
graph = new B2ConstraintGraph();
4646
graph.colors = new B2GraphColor[B2_GRAPH_COLOR_COUNT];
4747

48-
bodyCapacity = b2MaxInt(bodyCapacity, 8);
48+
int bodyCapacity = b2MaxInt(capacity.staticBodyCount + capacity.dynamicBodyCount, 16);
4949

5050
// Initialize graph color bit set.
5151
// No bitset for overflow color.
@@ -54,6 +54,7 @@ public static void b2CreateGraph(ref B2ConstraintGraph graph, int bodyCapacity)
5454
ref B2GraphColor color = ref graph.colors[i];
5555
color.bodySet = b2CreateBitSet(bodyCapacity);
5656
color.contactSims = b2Array_Create<B2ContactSim>();
57+
b2Array_Reserve(ref color.contactSims, 16);
5758
color.jointSims = b2Array_Create<B2JointSim>();
5859

5960
b2SetBitCountAndClear(ref color.bodySet, bodyCapacity);
@@ -69,6 +70,14 @@ public static void b2CreateGraph(ref B2ConstraintGraph graph, int bodyCapacity)
6970
}
7071
}
7172

73+
public static void b2CreateGraph(ref B2ConstraintGraph graph, int bodyCapacity)
74+
{
75+
b2CreateGraph(ref graph, new B2Capacity
76+
{
77+
staticBodyCount = bodyCapacity,
78+
});
79+
}
80+
7281
public static void b2DestroyGraph(ref B2ConstraintGraph graph)
7382
{
7483
for (int i = 0; i < B2_GRAPH_COLOR_COUNT; ++i)
@@ -354,10 +363,17 @@ internal static void b2RemoveJointFromGraph(B2World world, int bodyIdA, int body
354363

355364
internal static readonly B2HexColor[] b2_graphColors = new B2HexColor[]
356365
{
357-
B2HexColor.b2_colorRed, B2HexColor.b2_colorOrange, B2HexColor.b2_colorYellow, B2HexColor.b2_colorGreen, B2HexColor.b2_colorCyan, B2HexColor.b2_colorBlue,
358-
B2HexColor.b2_colorViolet, B2HexColor.b2_colorPink, B2HexColor.b2_colorChocolate, B2HexColor.b2_colorGoldenRod, B2HexColor.b2_colorCoral, B2HexColor.b2_colorRosyBrown,
359-
B2HexColor.b2_colorAqua, B2HexColor.b2_colorPeru, B2HexColor.b2_colorLime, B2HexColor.b2_colorGold, B2HexColor.b2_colorPlum, B2HexColor.b2_colorSnow,
360-
B2HexColor.b2_colorTeal, B2HexColor.b2_colorKhaki, B2HexColor.b2_colorSalmon, B2HexColor.b2_colorPeachPuff, B2HexColor.b2_colorHoneyDew, B2HexColor.b2_colorBlack,
366+
B2HexColor.b2_colorRed, B2HexColor.b2_colorOrange, B2HexColor.b2_colorYellow, B2HexColor.b2_colorLimeGreen, B2HexColor.b2_colorSpringGreen,
367+
B2HexColor.b2_colorAqua, B2HexColor.b2_colorDodgerBlue, B2HexColor.b2_colorBlueViolet, B2HexColor.b2_colorMagenta, B2HexColor.b2_colorDeepPink,
368+
B2HexColor.b2_colorCrimson, B2HexColor.b2_colorCoral, B2HexColor.b2_colorGold, B2HexColor.b2_colorGreenYellow, B2HexColor.b2_colorMediumSeaGreen,
369+
B2HexColor.b2_colorTurquoise, B2HexColor.b2_colorDeepSkyBlue, B2HexColor.b2_colorCornflowerBlue, B2HexColor.b2_colorMediumSlateBlue, B2HexColor.b2_colorMediumOrchid,
370+
B2HexColor.b2_colorHotPink, B2HexColor.b2_colorTomato, B2HexColor.b2_colorKhaki, B2HexColor.b2_colorSilver,
361371
};
372+
373+
public static B2HexColor b2GetGraphColor(int index)
374+
{
375+
B2_ASSERT(0 <= index && index < B2_GRAPH_COLOR_COUNT);
376+
return b2_graphColors[index];
377+
}
362378
}
363-
}
379+
}

src/Box2D.NET/B2Counters.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,5 +18,11 @@ public struct B2Counters
1818
public int byteCount;
1919
public int taskCount;
2020
public B2FixedArray24<int> colorCounts;
21+
22+
// Number of contacts touched by the collide pass (graph contacts + awake-set non-touching).
23+
public int awakeContactCount;
24+
25+
// Number of contacts recycled in the most recent step.
26+
public int recycledContactCount;
2127
}
2228
}

src/Box2D.NET/B2DynamicTrees.cs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,10 @@ internal static ushort b2MaxUInt16(ushort a, ushort b)
5353
}
5454

5555
/// Constructing the tree initializes the node pool.
56-
public static B2DynamicTree b2DynamicTree_Create()
56+
public static B2DynamicTree b2DynamicTree_Create(int proxyCapacity = 16)
5757
{
58+
int capacity = b2MaxInt(proxyCapacity, 16);
59+
5860
B2DynamicTree tree = new B2DynamicTree();
5961
tree.Clear();
6062

@@ -63,7 +65,8 @@ public static B2DynamicTree b2DynamicTree_Create()
6365

6466
tree.root = B2_NULL_INDEX;
6567

66-
tree.nodeCapacity = 16;
68+
// maximum node count for a full binary tree is 2 * leafCount - 1
69+
tree.nodeCapacity = 2 * capacity - 1;
6770
tree.nodeCount = 0;
6871
tree.nodes = b2Alloc<B2TreeNode>(tree.nodeCapacity);
6972

src/Box2D.NET/B2GraphColor.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,5 +29,7 @@ public struct B2GraphColor
2929
public ArraySegment<B2ContactConstraintWide> wideConstraints;
3030
public ArraySegment<B2ContactConstraint> overflowConstraints;
3131
//};
32+
33+
public int wideConstraintCount;
3234
}
3335
}

src/Box2D.NET/B2Profile.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ public struct B2Profile
1212
public float pairs;
1313
public float collide;
1414
public float solve;
15-
public float prepareStages;
16-
public float solveConstraints;
15+
public float solverSetup;
16+
public float constraints;
1717
public float prepareConstraints;
1818
public float integrateVelocities;
1919
public float warmStart;

src/Box2D.NET/B2Solvers.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1704,7 +1704,7 @@ internal static void b2Solve(B2World world, B2StepContext stepContext)
17041704
stepContext.stages = stages;
17051705
b2AtomicStoreU32(ref stepContext.atomicSyncBits, 0);
17061706

1707-
world.profile.prepareStages = b2GetMillisecondsAndReset(ref prepareTicks);
1707+
world.profile.solverSetup = b2GetMillisecondsAndReset(ref prepareTicks);
17081708
b2TracyCZoneEnd(B2TracyCZone.prepare_stages);
17091709

17101710
b2TracyCZoneNC(B2TracyCZone.solve_constraints, "Solve Constraints", B2HexColor.b2_colorIndigo, true);
@@ -1752,7 +1752,7 @@ internal static void b2Solve(B2World world, B2StepContext stepContext)
17521752
}
17531753
}
17541754

1755-
world.profile.solveConstraints = b2GetMillisecondsAndReset(ref constraintTicks);
1755+
world.profile.constraints = b2GetMillisecondsAndReset(ref constraintTicks);
17561756
b2TracyCZoneEnd(B2TracyCZone.solve_constraints);
17571757

17581758
b2TracyCZoneNC(B2TracyCZone.update_transforms, "Update Transforms", B2HexColor.b2_colorMediumSeaGreen, true);
@@ -2169,4 +2169,4 @@ internal static void b2Solve(B2World world, B2StepContext stepContext)
21692169
}
21702170
}
21712171
}
2172-
}
2172+
}

src/Box2D.NET/B2TaskContext.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,12 @@ public class B2TaskContext
1313
// These bits align with the contact id capacity and signal a change in contact status
1414
public B2BitSet contactStateBitSet;
1515

16+
// These bits align with the contact id capacity and signal a hit event.
17+
public B2BitSet hitEventBitSet;
18+
19+
// Fast-path flag: true when this worker set at least one bit in hitEventBitSet this step.
20+
public bool hasHitEvents;
21+
1622
// These bits align with the joint id capacity and signal a change in contact status
1723
public B2BitSet jointStateBitSet;
1824

@@ -26,5 +32,8 @@ public class B2TaskContext
2632
// Per worker split island candidate
2733
public float splitSleepTime;
2834
public int splitIslandId;
35+
36+
// Number of contacts recycled this step (collide pass).
37+
public int recycledContactCount;
2938
}
3039
}

0 commit comments

Comments
 (0)