Skip to content

Commit 679d2ed

Browse files
committed
Added ~AsSpan properties for arrays.
1 parent 0269bb7 commit 679d2ed

4 files changed

Lines changed: 109 additions & 44 deletions

File tree

Box2dNet/Box2dNet.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
<RepositoryUrl>https://github.com/thomasvt/Box2D3Net</RepositoryUrl>
1515
<GenerateDocumentationFile>true</GenerateDocumentationFile>
1616
<NoWarn>$(NoWarn);1591</NoWarn> <!-- suppress warnings for missing docs -->
17-
<Version>3.1.4.0</Version>
17+
<Version>3.1.5</Version>
1818
</PropertyGroup>
1919

2020
<ItemGroup>

Box2dNet/IntPtrExtensions.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ public static class IntPtrExtensions
88
/// <summary>
99
/// Gets a readonly span of a native array of which you know the length.
1010
/// </summary>
11+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
1112
public static unsafe ReadOnlySpan<T> NativeArrayAsSpan<T>(this IntPtr intPtr, int count) where T : struct
1213
{
1314
return MemoryMarshal.CreateReadOnlySpan(ref Unsafe.AsRef<T>(intPtr.ToPointer()), count);
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
using System.Numerics;
2+
3+
namespace Box2dNet.Interop
4+
{
5+
public partial struct b2ChainDef
6+
{
7+
/// <summary>
8+
/// Allows to read the points directly from native memory as a span.
9+
/// </summary>
10+
public ReadOnlySpan<Vector2> pointsAsSpan => points.NativeArrayAsSpan<Vector2>(count);
11+
12+
/// <summary>
13+
/// Allows to read the materials directly from native memory as a span.
14+
/// </summary>
15+
public ReadOnlySpan<b2SurfaceMaterial> materialsAsSpan => materials.NativeArrayAsSpan<b2SurfaceMaterial>(materialCount);
16+
}
17+
18+
public partial struct b2SensorEvents
19+
{
20+
/// <summary>
21+
/// Allows to read the beginEvents directly from native memory as a span.
22+
/// </summary>
23+
public ReadOnlySpan<b2SensorBeginTouchEvent> beginEventsAsSpan =>
24+
beginEvents.NativeArrayAsSpan<b2SensorBeginTouchEvent>(beginCount);
25+
26+
/// <summary>
27+
/// Allows to read the endEvents directly from native memory as a span.
28+
/// </summary>
29+
public ReadOnlySpan<b2SensorEndTouchEvent> endEventsAsSpan =>
30+
endEvents.NativeArrayAsSpan<b2SensorEndTouchEvent>(endCount);
31+
32+
}
33+
34+
public partial struct b2ContactEvents
35+
{
36+
/// <summary>
37+
/// Allows to read the beginEvents directly from native memory as a span.
38+
/// </summary>
39+
public ReadOnlySpan<b2ContactBeginTouchEvent> beginEventsAsSpan =>
40+
beginEvents.NativeArrayAsSpan<b2ContactBeginTouchEvent>(beginCount);
41+
42+
/// <summary>
43+
/// Allows to read the endEvents directly from native memory as a span.
44+
/// </summary>
45+
public ReadOnlySpan<b2ContactEndTouchEvent> endEventsAsSpan =>
46+
endEvents.NativeArrayAsSpan<b2ContactEndTouchEvent>(endCount);
47+
48+
/// <summary>
49+
/// Allows to read the hitEvents directly from native memory as a span.
50+
/// </summary>
51+
public ReadOnlySpan<b2ContactHitEvent> hitEventsAsSpan =>
52+
hitEvents.NativeArrayAsSpan<b2ContactHitEvent>(hitCount);
53+
}
54+
55+
public partial struct b2BodyEvents
56+
{
57+
/// <summary>
58+
/// Allows to read the moveEvents directly from native memory as a span.
59+
/// </summary>
60+
public ReadOnlySpan<b2BodyMoveEvent> moveEventsAsSpan =>
61+
moveEvents.NativeArrayAsSpan<b2BodyMoveEvent>(moveCount);
62+
}
63+
}

README.md

Lines changed: 44 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -45,62 +45,55 @@ I am using Box2dNet for my own game so you can expect me to add more convenience
4545
* the timer functions (b2CreateTimer, ..): use .NET timers :)
4646
* b2DynamicTree_X: little value for much effort on my side. This is the spatial tree used internally by Box2D. Erin exposed these because people may want to use the tree elsewhere, but you don't need these functions for normal Box2D use.
4747

48-
# Dealing with pointers (IntPtr)
48+
# Multi-threading support
4949

50-
The largest down-side of PInvoke wrappers is that many C pointers become `IntPtr` in .NET. Because of this, the type that was there in C of the `struct` or `delegate` is lost and replaced by `IntPtr` in C#.
50+
Box2dNet comes with .NET integration for the new multi-threaded task system that Box2D uses.
5151

52-
To help with this, Box2dNet mentions the original C type in the C# generated comments wherever possible. Code completion should therefore show this information. Worst case, you'll have to look in the sources here on gitHub.
52+
Simply use `B2Api.b2DefaultWorldDef_WithDotNetTpl()` instead of `B2Api.b2DefaultWorldDef` to create your Box2D world:
5353

54-
To help you with IntPtrs, the following sections show solutions for most use cases:
54+
``` C#
55+
var worldDef = useMultiThreading
56+
? B2Api.b2DefaultWorldDef_WithDotNetTpl() // <---- this is all it takes for default multi threading
57+
: B2Api.b2DefaultWorldDef();
5558

56-
## Callbacks: setting struct.fields that are callback delegates.
59+
var b2WorldId = B2Api.b2CreateWorld(worldDef);
60+
```
5761

58-
> As of Box2dNet v3.1.5 all Box2D functions that have callback delegates parameters have a companion overload that takes in the strongly typed delegate. eg. use `b2World_SetPreSolveCallback(b2WorldId worldId, **b2PreSolveFcn fcn**, IntPtr context)`, not `b2World_SetPreSolveCallback(b2WorldId worldId, **IntPtr fcn**, IntPtr context)`
62+
Note that multithreading incurs quite some overhead so you only gain net-profit when you simulate a lot of bodies. Measure your specific use cases!
5963

60-
Some Box2D struct fields are delegate pointers. These are `IntPtr` and must be set to a pointer to a method with the same definition as the delegate requires.
64+
> the 'TPL' in `b2DefaultWorldDef_WithDotNetTpl` stands for Task Parallel Library, which is the name of .NET's Task framework.
6165
62-
To do this, you must:
66+
# Dealing with pointers (IntPtr)
6367

64-
* check the generated comments to find the identifier of the original C delegate type
65-
* write or generate a method that matches the delegate
66-
* assign a function pointer retrieved with `Marshal.GetFunctionPointerForDelegate` to the IntPtr struct field.
68+
The largest down-side of PInvoke wrappers is that many C pointers become `IntPtr` in .NET. Because of this, the type that was there in C of the `struct` or `delegate` is lost and replaced by `IntPtr` in C#.
6769

68-
Here is an example:
70+
To help with this, Box2dNet has several helpers to deal with IntPtr, or to remove the need to deal with them at all.
6971

70-
``` C#
71-
...
72-
var worldDef = b2DefaultWorldDef();
73-
worldDef.enqueueTask = Marshal.GetFunctionPointerForDelegate((b2EnqueueTaskCallback)EnqueueTaskCallback);
74-
}
72+
Several situations remain, though, where you need to do it yourself. The following sections show solutions for most of these cases.
7573

76-
private static IntPtr EnqueueTaskCallback(IntPtr /* b2TaskCallback */ task, int itemCount, int minRange, IntPtr taskContext, IntPtr userContext)
77-
{
78-
...
79-
}
80-
```
74+
The original C type of the pointer is in the generated comments. Code completion should show this information. Worst case, you'll have to look in the sources here on gitHub.
8175

8276
## Reading native arrays from IntPtr (without copying)
8377

84-
Some structs received from native Box2D contain arrays. To read those arrays Box2dNet provides convenience method `NativeArrayAsSpan` to loop over the native contents without making temporary copies or allocating an iterator.
78+
Some structs received from native Box2D contain IntPtrs to arrays. Box2dNet provides companion properties called `~AsSpan` for these with which you can read the data directly from native memory, strongly typed (so no allocations for copies or iterators).
8579

86-
Example: field `IntPtr b2ContactEvents.beginEvents` shows in its comment that you should read it as an array of `b2ContactBeginTouchEvent`:
80+
> If this companion property is not there, you can convert the IntPtr yourself to a Span using Box2dNet's convenience method `NativeArrayAsSpan(count)` which is simply what the `~AsSpan` does behind the scenes.
81+
82+
Example: struct `b2ContactEvents` contains an array in field `IntPtr beginEvents` which you can read easily using companion `beginEventsAsSpan`:
8783

8884
``` C#
8985
var hitEvents = B2Api.b2World_GetContactEvents(b2WorldId);
90-
// use helper extension method to efficiently read the native hitEvents array with little code:
91-
foreach (var @event in hitEvents.beginEvents.NativeArrayAsSpan<b2ContactBeginTouchEvent>(hitEvents.beginCount))
86+
foreach (var @event in **hitEvents.beginEventsAsSpan**)
9287
{
9388
Console.WriteLine($"!!!!!!! HIT detected between {@event.shapeIdA} and {@event.shapeIdB}");
9489
}
9590
```
9691

97-
> Note that you must know the size of the array, which is always provided by Box2D in a sibling field.
98-
9992
## Pass a reference to a .NET object into native Box2D as IntPtr (eg. UserData)
10093

101-
If you want to pass a .NET object reference into native Box2D, like when tagging a Shape or Body with `userData`, you must pass in an `IntPtr` to the object. But in .NET objects can get relocated by the memory manager.
94+
If you want to pass a .NET object reference into native Box2D, like when tagging a Shape or Body with `userData`, you must pass in an `IntPtr` to the object. But .NET objects are not guaranteed to stay at the same address in memory.
10295

103-
To solution is to allocate a `NativeHandle` for your .NET object and pass the handle's `IntPtr` to Box2D instead of a direct pointer to the instance. Example:
96+
The solution is to allocate a `NativeHandle` for your .NET object and pass *that* to Box2D. Example:
10497

10598
``` C#
10699
_handle = NativeHandle.Alloc(ball); // allocate a IntPtr handle for the .NET object and return it as IntPtr.
@@ -112,7 +105,7 @@ var circle = new b2Circle() { radius = 1 };
112105
var b2ShapeId = B2Api.b2CreateCircleShape(b2BodyId, in shapeDef, in circle);
113106
```
114107

115-
After this, when Box2D passes the `IntPtr` back to .NET somewhere, for instance when you call `b2X_GetUserData()`, you can get hold of the corresponding .NET object like this:
108+
After this, when Box2D passes the `IntPtr` back to .NET somewhere (eg. through `b2X_GetUserData()`) you can get hold of the corresponding .NET object like this:
116109

117110
``` C#
118111
var userDataIntPtr = B2Api.b2Shape_GetUserData(shapeId);
@@ -126,25 +119,33 @@ When you're fully done with it, eg. when the game object is removed from your ga
126119
NativeHandle.Free(_handle);
127120
```
128121

129-
> A tip to avoid NativeHandle with UserData: abuse the UserData poointer by setting it to your gameobject's numerical ID (a normal int or long). IntPtr is just a numerical value, it doesn't have to be an actual pointer address: `new IntPtr(entity.Id)` works just fine.
122+
> A tip to avoid needing to use NativeHandle with UserData: abuse the UserData pointer by setting it to your gameobject's numerical ID (a normal int or long). IntPtr is just a numerical value, it doesn't have to be an actual pointer address: `new IntPtr(entity.Id)` works just fine.
130123
131-
# Multi-threading support
124+
## Callbacks: setting struct.fields that are callback delegates.
132125

133-
Box2dNet comes with .NET integration for the new multi-threaded task system that Box2D uses.
126+
> As of Box2dNet v3.1.5 all Box2D functions that have callback delegates parameters have a companion overload that takes in the strongly typed delegate. eg. use `b2World_SetPreSolveCallback(b2WorldId worldId, **b2PreSolveFcn fcn**, IntPtr context)`, not `b2World_SetPreSolveCallback(b2WorldId worldId, **IntPtr fcn**, IntPtr context)`
134127
135-
Simply use `B2Api.b2DefaultWorldDef_WithDotNetTpl()` instead of `B2Api.b2DefaultWorldDef` to create your Box2D world:
128+
Some Box2D struct fields are delegate pointers. These are `IntPtr` and must be set to a pointer to a method with the same definition as the delegate requires.
136129

137-
``` C#
138-
var worldDef = useMultiThreading
139-
? B2Api.b2DefaultWorldDef_WithDotNetTpl() // <---- this is all it takes for default multi threading
140-
: B2Api.b2DefaultWorldDef();
130+
To do this, you must:
141131

142-
var b2WorldId = B2Api.b2CreateWorld(worldDef);
143-
```
132+
* check the generated comments to find the identifier of the original C delegate type
133+
* write or generate a method that matches the delegate
134+
* assign a function pointer retrieved with `Marshal.GetFunctionPointerForDelegate` to the IntPtr struct field.
144135

145-
Note that multithreading incurs quite some overhead so you only gain net-profit when you simulate a lot of bodies. Measure your specific use cases!
136+
Here is an example:
146137

147-
> the 'TPL' in `b2DefaultWorldDef_WithDotNetTpl` stands for Task Parallel Library, which is the name of .NET's Task framework.
138+
``` C#
139+
...
140+
var worldDef = b2DefaultWorldDef();
141+
worldDef.enqueueTask = Marshal.GetFunctionPointerForDelegate((b2EnqueueTaskCallback)EnqueueTaskCallback);
142+
}
143+
144+
private static IntPtr EnqueueTaskCallback(IntPtr /* b2TaskCallback */ task, int itemCount, int minRange, IntPtr taskContext, IntPtr userContext)
145+
{
146+
...
147+
}
148+
```
148149

149150
# Samples
150151

0 commit comments

Comments
 (0)