Skip to content

Commit f348397

Browse files
committed
[upstream] Threading cleanup (erincatto/box2d#1049)
- Added internal scheduler so users don't need one. - Simplified b2EnqueueTaskCallback. Users are no longer responsible for providing parallel-for. - Simplified b2TaskCallback. Users no longer need to provide a thread/worker index. - Removed usage of enkiTS from the samples app. It was very useful, but I needed more control over threading. Users can now refer to scheduler.h for example task scheduler hook up. - Cleaned up and documented the solver tasking system - Added b2World_SetWorkerCount and b2World_GetWorkerCount so the worker count can be modified on an existing world - Added `B2_MAX_TASKS` so users can pre-allocate task arrays with stable pointers.
1 parent 89908ec commit f348397

32 files changed

Lines changed: 1040 additions & 739 deletions
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# Porting Review: box2d ddfd9df727a06940af34b5bc2ef79bcaba287d50
2+
3+
Original commit: `ddfd9df727a06940af34b5bc2ef79bcaba287d50` (`Threading cleanup (#1049)`)
4+
5+
## Summary
6+
7+
This port applies the runtime meaning of the threading cleanup to Box2D.NET:
8+
9+
- `b2TaskCallback` and `b2EnqueueTaskCallback` no longer expose parallel-for ranges to user task systems.
10+
- Box2D.NET now owns range partitioning through `B2ParallelFors.b2ParallelFor`.
11+
- A built-in managed scheduler with long-lived worker threads is used when `workerCount > 1` and no external task callbacks are supplied.
12+
- `B2_MAX_WORKERS` is reduced to 32 and `B2_MAX_TASKS` is added.
13+
- Worker contexts can be recreated through `b2World_SetWorkerCount` / `b2World_GetWorkerCount`.
14+
- Solver task block initialization and comments were updated to preserve the original cleanup/documentation intent.
15+
16+
## File Mapping
17+
18+
| Original file | C# counterpart | Status | Meaning difference / notes | Remaining risk |
19+
|---|---|---:|---|---|
20+
| `include/box2d/types.h` | `src/Box2D.NET/B2Delegates.cs`, `src/Box2D.NET/B2WorldDef.cs` | Ported | Task delegates simplified to one task context; worker-count documentation updated. | Public API break matches upstream. |
21+
| `include/box2d/constants.h` | `src/Box2D.NET/B2Constants.cs` | Ported | `B2_MAX_WORKERS = 32`, `B2_MAX_TASKS = 256`. XML/doc-comment style remains C# style. | Existing users expecting 64 workers will be clamped to 32. |
22+
| `include/box2d/box2d.h` | `src/Box2D.NET/B2Worlds.cs` | Ported | Added `b2World_SetWorkerCount` and `b2World_GetWorkerCount`. | External task callbacks keep ownership of their own scheduler. |
23+
| `src/parallel_for.c`, `src/parallel_for.h` | `src/Box2D.NET/B2ParallelFor.cs` | Ported | Same atomic block-claiming algorithm; C# class contexts preserve stable worker index. | Managed task scheduling can differ in physical thread affinity. |
24+
| `src/scheduler.c`, `src/scheduler.h`, `src/core.h`, `src/timer.c` thread primitives | `src/Box2D.NET/B2Scheduler.cs`, .NET `Thread`/`SemaphoreSlim` infrastructure | Ported with .NET equivalent | Native thread/semaphore APIs are not exposed publicly in Box2D.NET; the built-in scheduler mirrors the fixed task array, worker threads, semaphore wakeup, and main-thread helping model. | OS-level details differ from C, but scheduler ownership and task lifetime now match closely. |
25+
| `src/physics_world.c`, `src/physics_world.h` | `src/Box2D.NET/B2World.cs`, `src/Box2D.NET/B2Worlds.cs` | Ported | Worker context create/destroy helpers added; built-in scheduler selected when no external callbacks are provided. | C# `B2TaskContext`/`B2SensorTaskContext` are classes, so recreation deliberately destroys and reallocates per-worker bitsets. |
26+
| `src/broad_phase.c` | `src/Box2D.NET/B2BroadPhases.cs` | Ported | Pair finding uses `b2ParallelFor`; tree rebuild uses simplified task callback; move-pair capacity changed from `8x` to `32x`. | Tree rebuild task falls back inline if `B2_MAX_TASKS` is exceeded. |
27+
| `src/sensor.c`, `src/sensor.h` | `src/Box2D.NET/B2Sensors.cs`, existing sensor context types | Ported | Sensor overlap work uses `b2ParallelFor` and internal worker indexes. | None known. |
28+
| `src/island.c`, `src/island.h` | `src/Box2D.NET/B2Islands.cs` | Ported | `b2SplitIslandTask` uses single task context callback. | None known. |
29+
| `src/solver.c`, `src/solver.h` | `src/Box2D.NET/B2Solvers.cs`, `src/Box2D.NET/B2SolverBlock.cs` | Ported | Solver block documentation preserved; block/stage initialization helpers added; finalize/bullet tasks use `b2ParallelFor`; solver tasks use simplified callback. | Managed runtime placement can still differ from native cache affinity. |
30+
| `src/contact_solver.c`, `src/contact_solver.h` | `src/Box2D.NET/B2ContactSolvers.cs` | Ported | `b2GetContactConstraintSIMDByteCount` renamed to `b2GetWideContactConstraintByteCount`. | C# allocation is element-count based, so byte count is kept as semantic/API counterpart, not used for pointer arithmetic. |
31+
| `src/container.h`, `src/array.h` | `src/Box2D.NET/B2Arrays.cs` | Already represented / partially ported | Existing generic arrays already distinguish class vs struct allocation; worker context recreation handles zero-initialization semantics explicitly. | No new generic resize-zero helper added because class field ownership must be destroyed deliberately. |
32+
| `test/test_determinism.c` | `test/Box2D.NET.Test/B2DeterminismTest.cs` | Ported | Determinism now exercises built-in scheduler over wider worker-count range. | More worker-count coverage increases runtime. |
33+
| `test/test_world.c` | `test/Box2D.NET.Test/B2WorldTest.cs` | Ported | Added `TestSetWorkerCount` with Junkyard stepping and clamp checks. | None known. |
34+
| `test/test_thread.c` | Existing .NET runtime / `B2Scheduler.cs`; no direct public core API | Not directly ported | Native `b2Thread`/`b2Semaphore` APIs do not exist in Box2D.NET. The relevant scheduler behavior is covered by determinism and worker-count tests. | Lower direct coverage of semaphore/thread wrappers because they are not public C# wrappers. |
35+
| `test/test_container.c`, `test/test_shape.c`, `test/main.c`, `test/CMakeLists.txt` | Existing NUnit tests / `.csproj` discovery | No direct code changes | Original changes are C test harness/build registration. NUnit auto-discovers tests. | None. |
36+
| `samples/*.cpp`, `samples/*.c`, `samples/*.h`, `samples/CMakeLists.txt` | `src/Box2D.NET.Samples/Samples/Sample.cs` | Ported where relevant | Samples no longer keep or pass task callbacks into worlds; built-in scheduler is used. | None known. |
37+
| `benchmark/main.c`, benchmark CSV files | `tools/Box2D.NET.Benchmark`, `src/Box2D.NET.Shared/*Data.cs` | Not directly ported | Original benchmark CSV and C harness updates do not map to the current .NET benchmark data flow in this branch. | Benchmark baselines may not reflect upstream m2air_neon CSV updates. |
38+
| `shared/random.c`, `shared/random.h`, `shared/utils.c`, `shared/utils.h`, `shared/human.c` | `src/Box2D.NET.Shared/RandomSupports.cs`, `src/Box2D.NET.Shared/Humans.cs` | Already represented / no runtime change needed | C rename from random to utils is build-organization cleanup; C# shared helpers are already split by domain. | No known runtime risk. |
39+
| `CMakeLists.txt`, `.gitignore`, `README.md`, `docs/foundation.md`, `docs/samples.md`, `src/CMakeLists.txt`, `benchmark/CMakeLists.txt`, `shared/CMakeLists.txt` | `.csproj`, `.sln`, `README.md`, `docs/*` | Reviewed, mostly not applicable | CMake/enkiTS removal has no direct .NET build counterpart. README/docs wording was not broadly rewritten because Box2D.NET docs are not 1:1 with upstream C docs. | Documentation may still mention old task callback details if present outside updated API comments. |
40+
41+
## Special Handling
42+
43+
- `B2TaskContext` and `B2SensorTaskContext` are C# classes. The port does not rely on shallow value-copy semantics; worker context lifetime is managed by destroy/recreate helpers.
44+
- `b2World_SetWorkerCount` switches a serial world to the built-in scheduler when the count rises above 1. This avoids a C#-specific deadlock where the serial callback would run worker 0 inline before other solver workers are enqueued.
45+
- Parallel-for worker identity is assigned internally by `B2ParallelForTask.workerIndex`, matching the upstream rule that user task systems no longer provide worker indexes.
46+
- The built-in scheduler uses managed `Thread` and `SemaphoreSlim` rather than native `b2Thread`/`b2Semaphore` wrappers, but keeps the upstream fixed worker and task-claiming structure.
47+
48+
## Verification
49+
50+
- `dotnet test test/Box2D.NET.Test/Box2D.NET.Test.csproj --framework net10.0 --no-restore`: passed, 163 tests.
51+
- `dotnet build Box2D.NET.sln --no-restore`: passed with pre-existing warnings.

src/Box2D.NET.Samples/Primitives/SampleTask.cs

Lines changed: 0 additions & 24 deletions
This file was deleted.

src/Box2D.NET.Samples/Primitives/TaskScheduler.cs

Lines changed: 0 additions & 81 deletions
This file was deleted.

src/Box2D.NET.Samples/SampleApp.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
using Silk.NET.OpenGL.Extensions.ImGui;
2020
using Silk.NET.Windowing;
2121
using static Box2D.NET.B2Cores;
22+
using static Box2D.NET.B2Constants;
2223
using static Box2D.NET.B2Diagnostics;
2324
using static Box2D.NET.B2Buffers;
2425
using static Box2D.NET.B2MathFunction;
@@ -758,7 +759,7 @@ private unsafe void ScrollCallback(WindowHandle* window, double dx, double dy)
758759

759760
private void UpdateUI()
760761
{
761-
int maxWorkers = (int)(Environment.ProcessorCount * 1.5f);
762+
int maxWorkers = B2_MAX_WORKERS;
762763

763764
float fontSize = ImGui.GetFontSize();
764765
float menuWidth = 13.0f * fontSize;

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

Lines changed: 5 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,7 @@ namespace Box2D.NET.Samples.Samples;
2727

2828
public class Sample : IDisposable
2929
{
30-
public const int k_maxContactPoints = 12 * 2048;
31-
public const int m_maxTasks = 64;
30+
public const int m_maxTasks = 512;
3231
public const int m_maxThreads = 64;
3332
public const int m_profileCapacity = 512;
3433

@@ -42,11 +41,6 @@ public class Sample : IDisposable
4241
protected Camera m_camera;
4342
protected Draw m_draw;
4443

45-
private TaskScheduler m_scheduler;
46-
private SampleTask[] m_tasks;
47-
private int m_taskCount;
48-
protected int m_threadCount;
49-
5044
private B2BodyId m_mouseBodyId;
5145

5246
//
@@ -79,19 +73,6 @@ public Sample(SampleContext context)
7973
m_camera = context.camera;
8074
m_draw = context.draw;
8175

82-
m_scheduler = new TaskScheduler();
83-
m_scheduler.Initialize(m_context.workerCount);
84-
85-
m_tasks = new SampleTask[m_maxTasks];
86-
for (int i = 0; i < m_maxTasks; ++i)
87-
{
88-
m_tasks[i] = new SampleTask();
89-
}
90-
91-
m_taskCount = 0;
92-
93-
m_threadCount = 1 + m_context.workerCount;
94-
9576
m_worldId = b2_nullWorldId;
9677

9778
m_textIncrement = 26;
@@ -125,7 +106,7 @@ public virtual void Dispose()
125106
{
126107
// By deleting the world, we delete the bomb, mouse joint, etc.
127108
b2DestroyWorld(m_worldId);
128-
109+
129110
// delete m_scheduler;
130111
// delete[] m_tasks;
131112
}
@@ -140,8 +121,8 @@ public void CreateWorld()
140121

141122
B2WorldDef worldDef = b2DefaultWorldDef();
142123
worldDef.workerCount = m_context.workerCount;
143-
worldDef.enqueueTask = EnqueueTask;
144-
worldDef.finishTask = FinishTask;
124+
// worldDef.enqueueTask = EnqueueTask;
125+
// worldDef.finishTask = FinishTask;
145126
worldDef.userTaskContext = this;
146127
worldDef.enableSleep = m_context.enableSleep;
147128

@@ -340,41 +321,6 @@ private void DrawProfileSeries(ImDrawListPtr drawList, Vector2 origin, Vector2 s
340321
previous = current;
341322
}
342323
}
343-
344-
345-
private static object EnqueueTask(b2TaskCallback task, int itemCount, int minRange, object taskContext, object userContext)
346-
{
347-
Sample sample = userContext as Sample;
348-
if (sample.m_taskCount < m_maxTasks)
349-
{
350-
SampleTask sampleTask = sample.m_tasks[sample.m_taskCount];
351-
sampleTask.m_SetSize = itemCount;
352-
sampleTask.m_MinRange = minRange;
353-
sampleTask.m_task = task;
354-
sampleTask.m_taskContext = taskContext;
355-
sample.m_scheduler.AddTaskSetToPipe(sampleTask);
356-
++sample.m_taskCount;
357-
return sampleTask;
358-
}
359-
else
360-
{
361-
// This is not fatal but the maxTasks should be increased
362-
B2_ASSERT(false);
363-
task(0, itemCount, 0, taskContext);
364-
return null;
365-
}
366-
}
367-
368-
private static void FinishTask(object taskPtr, object userContext)
369-
{
370-
if (taskPtr != null)
371-
{
372-
SampleTask sampleTask = taskPtr as SampleTask;
373-
Sample sample = userContext as Sample;
374-
sample.m_scheduler.WaitforTask(sampleTask);
375-
}
376-
}
377-
378324
public void ResetText()
379325
{
380326
m_textLine = m_textIncrement;
@@ -568,7 +514,7 @@ public virtual void Step()
568514
for (int i = 0; i < 1; ++i)
569515
{
570516
b2World_Step(m_worldId, timeStep, m_context.subStepCount);
571-
m_taskCount = 0;
517+
// m_taskCount = 0;
572518
}
573519

574520
if (timeStep > 0.0f)

src/Box2D.NET/B2Arrays.cs

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,9 @@
33
// SPDX-License-Identifier: MIT
44

55
using System;
6-
using System.Runtime.InteropServices;
76
using System.Runtime.CompilerServices;
87
using static Box2D.NET.B2Constants;
98
using static Box2D.NET.B2Buffers;
10-
using static Box2D.NET.B2Diagnostics;
119

1210
namespace Box2D.NET
1311
{
@@ -205,5 +203,12 @@ public static void b2Array_Destroy<T>(ref B2Array<T> a)
205203
a.count = 0;
206204
a.capacity = 0;
207205
}
206+
207+
public static void b2Array_ResizeAndSetZero<T>(ref B2Array<T> a, int n) where T : new()
208+
{
209+
b2Array_Reserve(ref a, n);
210+
// memset(0, ...)
211+
a.count = n;
212+
}
208213
}
209-
}
214+
}

0 commit comments

Comments
 (0)