Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,9 @@ path.
[![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)

**[Getting started](docs/getting-started.md)** ·
**[Guides](docs/guides/bodies.md)** ·
**[Examples](docs/examples.md)** ·
**[Gallery](docs/gallery.md)** ·
**[Architecture](docs/architecture.md)** ·
**[Benchmarks](docs/benchmarks.md)** ·
**[API reference](https://Miguel249.github.io/Box3D.NET/)**

Expand Down Expand Up @@ -462,10 +463,13 @@ All nine, animated, are in the **[gallery](docs/gallery.md)**.

| | |
| --- | --- |
| [Getting started](docs/getting-started.md) | From nothing to a simulation, and the handful of things that will otherwise trip you up. |
| [Gallery](docs/gallery.md) | Nine scenes, animated, and how the renderer that drew them hangs off the public interface. |
| [Architecture](docs/architecture.md) | Layers, ownership and the frame loop, with diagrams. |
| [Getting started](docs/getting-started.md) | Install, first simulation, the loop. |
| [Guides](docs/guides/bodies.md) | Bodies, shapes, filtering, queries, events, joints, terrain, characters, debug draw. |
| [Concepts](docs/concepts/step.md) | The step, memory and ownership, handle validity, threading, the native layer. |
| [Examples](docs/examples.md) | Sixteen runnable samples, and what each one teaches. |
| [Gallery](docs/gallery.md) | Nine scenes, animated, drawn through the public debug draw interface. |
| [Benchmarks](docs/benchmarks.md) | What the wrapper costs, measured. |
| [Architecture](docs/architecture.md) | How the binding is generated and held to the C API. |
| [API coverage](docs/api-coverage.md) | Every function Box3D exports, how it is bound, and whether the idiomatic layer reaches it. |
| [API reference](https://Miguel249.github.io/Box3D.NET/) | Every public type, generated from the XML documentation. |

Expand Down
48 changes: 30 additions & 18 deletions docs/api/index.md
Original file line number Diff line number Diff line change
@@ -1,24 +1,36 @@
# API reference

Two namespaces, and you almost certainly want the first.
Three namespaces, and you almost certainly want the first.

## Box3D
| Namespace | What it is |
| --- | --- |
| `Box3D` | The idiomatic surface. Start at `PhysicsWorld`, then `Body` and `Shape`. |
| `Box3D.Native` | A literal mirror of the Box3D C API: the same names, the same signatures, no abstraction. |
| `Box3D.Interop` | The one sanctioned bridge between the two, as extension methods. |

The idiomatic surface. Start at `PhysicsWorld`, then `Body` and `Shape`.

Everything here validates its input, manages nothing you did not ask it to
Everything in `Box3D` validates its input, manages nothing you did not ask it to
manage, and allocates nothing on the simulation path.

## Box3D.Native

A literal mirror of the Box3D C API: the same names, the same signatures, no
abstraction. Reach for it when you need one of the roughly 580 exported
functions the idiomatic surface does not cover yet.

Nothing here validates anything or manages a lifetime. Passing an invalid
identifier crashes the process rather than raising an exception.

## Box3D.Interop

The one sanctioned bridge between the two, as extension methods. Importing this
namespace is what makes reaching for the C layer visible in your own source.
Nothing in `Box3D.Native` validates anything or manages a lifetime. Passing an
invalid identifier crashes the process rather than raising an exception. Reach
for it when you need one of the roughly 580 exported functions the idiomatic
surface does not cover yet — [the native layer](../concepts/native-layer.md)
explains the boundary, and [API coverage](../api-coverage.md) lists what is on
each side of it.

Importing `Box3D.Interop` is what makes reaching for the C layer visible in your
own source.

## Where to start

| Looking for | Type |
| --- | --- |
| Creating and stepping a world | [`PhysicsWorld`](Box3D.PhysicsWorld.yml) |
| A physical object | [`Body`](Box3D.Body.yml), [`BodyDefinition`](Box3D.BodyDefinition.yml) |
| Collision geometry | [`Shape`](Box3D.Shape.yml), [`ShapeDefinition`](Box3D.ShapeDefinition.yml) |
| Ray casts and overlaps | [`RaycastHit`](Box3D.RaycastHit.yml), [`IRaycastCallback`](Box3D.IRaycastCallback.yml) |
| What happened last step | [`WorldEvents`](Box3D.WorldEvents.yml) |
| Constraints | [`Joint`](Box3D.Joint.yml) and the nine specific handles |
| Terrain | [`HeightField`](Box3D.HeightField.yml), [`CollisionMesh`](Box3D.CollisionMesh.yml) |
| Character movement | [`CharacterMover`](Box3D.CharacterMover.yml) |
| Drawing the simulation | [`IDebugDrawer`](Box3D.IDebugDrawer.yml) |
233 changes: 71 additions & 162 deletions docs/architecture.md
Original file line number Diff line number Diff line change
@@ -1,188 +1,97 @@
# Architecture

How the pieces fit, and why they are arranged this way.
How the binding is produced and how it is held to the C API. This page is for
contributors and for anyone deciding whether to trust the layer between their
game and Box3D.

## The layers
Using the library needs none of it: [the native layer](concepts/native-layer.md)
covers the packages and where the boundary is, and
[Memory and ownership](concepts/ownership.md) covers what owns what.

```mermaid
flowchart TB
app["Your game or application"]
high["<b>Box3D.NET</b><br/>PhysicsWorld · Body · Shape · Joint<br/>idiomatic, validated, allocation-free"]
interop["<b>Box3D.Interop</b><br/>ToNativeId · ToBody<br/><i>the marked door between layers</i>"]
native["<b>Box3D.NET.Native</b><br/>579 P/Invokes · blittable structs<br/>a literal mirror of the C API"]
c["<b>Box3D</b> (C)<br/>consumed unmodified as a submodule"]

app -->|"the normal path"| high
app -.->|"when you need<br/>something not wrapped"| interop
high --> native
interop --> native
native -->|"P/Invoke, no marshalling"| c

style high fill:#512BD4,color:#fff,stroke:#3a1f9e
style native fill:#b09ef5,color:#1a1a1a,stroke:#7d68c9
style interop fill:#fff,color:#1a1a1a,stroke:#512BD4,stroke-dasharray: 4 3
style c fill:#d6cdfa,color:#1a1a1a,stroke:#7d68c9
style app fill:#f4f4f5,color:#1a1a1a,stroke:#a1a1aa
```

The rule is that `Box3D.NET` never names a `Box3D.NET.Native` type in public
API. If it did, every consumer touching a handle would take a compile-time
dependency on the C ABI and the two packages could no longer version
independently. `LayeringTests` enforces it by reflection over the built
assembly, because a rule like this decays quietly.

Dropping to the native layer stays possible, because a thin wrapper should not
be a ceiling — Box3D exports around 580 functions and the idiomatic surface does
not cover all of them. But it goes through `Box3D.Interop`, so the coupling
appears as a `using` in your own source rather than happening by accident.

## What owns what

The single most important diagram, because it is the one thing you can get
wrong in a way that crashes rather than throws.
## The build

```mermaid
flowchart LR
subgraph disposable["Owns unmanaged memory — IDisposable"]
world["PhysicsWorld"]
mesh["CollisionMesh"]
field["HeightField"]
compound["CompoundGeometry"]
hull["ConvexHull"]
end

subgraph handles["Handles — copy freely, dispose nothing"]
body["Body"]
shape["Shape"]
joint["Joint"]
end

world -->|"creates and owns"| body
body -->|"creates and owns"| shape
world -->|"creates and owns"| joint

hull -.->|"<b>copied</b> on attach"| shape
mesh -->|"<b>borrowed</b> — must outlive"| shape
field -->|"<b>borrowed</b> — must outlive"| shape
compound -->|"<b>borrowed</b> — must outlive"| shape

style world fill:#512BD4,color:#fff
style mesh fill:#dc2626,color:#fff
style field fill:#dc2626,color:#fff
style compound fill:#dc2626,color:#fff
style hull fill:#16a34a,color:#fff
style body fill:#f4f4f5,color:#1a1a1a
style shape fill:#f4f4f5,color:#1a1a1a
style joint fill:#f4f4f5,color:#1a1a1a
```
sub["external/box3d<br/><i>submodule, pinned, never modified</i>"]
script["tools/build-native.ps1<br/>CMake · shared library"]
runtimes["runtimes/&lt;rid&gt;/native/"]
gen["tools/generate-bindings.ps1"]
generated["Generated/*.g.cs<br/>543 declarations"]
pkg["NuGet packages"]

Read the arrow colours:
sub --> script --> runtimes --> pkg
sub -->|"headers"| gen --> generated --> pkg

- **Green** — a hull is interned into the world when attached, so it may be
disposed the moment the shape exists.
- **Red** — a mesh, height field or baked compound is *borrowed*. The shape holds
a pointer into it. Disposing it while a shape is alive is a use-after-free
inside the solver. **Dispose the world first.**
- **Grey** — bodies, shapes and joints own nothing. They die with their world.
style sub fill:#d6cdfa,color:#1a1a1a
style pkg fill:#512BD4,color:#fff
```

```csharp
using var terrain = HeightField.FromHeights(heights, 256, 256, scale);
Box3D is a submodule pinned to a commit and never modified. Both the binding and
the binary are derived from it, which is what makes an upgrade a matter of
moving the submodule, re-running two scripts and reading the diff:

using (var world = new PhysicsWorld())
{
world.CreateStaticBody().AddHeightField(terrain);
Simulate(world);
}
// World disposed here, terrain after. Never the other way round.
```sh
git -C external/box3d checkout <commit>
pwsh tools/generate-bindings.ps1 # re-emit the P/Invokes and record the commit
pwsh tools/dump-abi.ps1 # re-record the struct layouts
dotnet test -c Release
```

None of the disposable types has a finalizer. A finalizer runs on the GC thread
at a time of the runtime's choosing, and freeing a world mid-step, or a mesh a
live shape still points at, corrupts rather than leaks. Forgetting to dispose
leaks until the process exits, which is a bug you can see; freeing early is one
you cannot.
CI fails if the checked-in generated sources differ from what the scripts
produce, which is the point of them.

## A frame
## The bindings are generated

```mermaid
sequenceDiagram
participant App as Your game
participant World as PhysicsWorld
participant Box3D as Box3D (C)

App->>World: body.LinearVelocity = v
Note over World: finite check, 0.11 ns
World->>Box3D: b3Body_SetLinearVelocity

App->>World: Step(1/60)
World->>Box3D: b3World_Step
Note over Box3D: collide · solve · integrate<br/>buffers events internally

App->>World: Events.BodyMoves
World->>Box3D: b3World_GetBodyEvents
Box3D-->>World: pointer + count
Note over World: ref struct view,<br/>no copy, no allocation
World-->>App: only the bodies that moved

App->>World: RaycastClosest(...)
World->>Box3D: b3World_CastRayClosest
Box3D-->>App: RaycastHit
```
`tools/generate-bindings.ps1` produces the 543 P/Invoke declarations from the
Box3D headers, converting the Doxygen comments into XML documentation along the
way. A mistyped parameter in a hand-written binding does not fail to compile; it
corrupts the stack at run time. Generating removes that class of bug.

Events are buffered by Box3D during the step and handed back afterwards rather
than raised as callbacks, because the solver is multithreaded and because
applications usually want to change the world in response — which is unsafe
mid-step. `WorldEvents` exposes them as `ref struct` views over engine memory,
so reading a frame's worth allocates nothing. They are valid only until the next
step.
A C type the script has not been taught is a hard error rather than something
passed through, and `BindingSource.Commit` records which Box3D revision the
declarations came from, so an assembly can be traced back to its headers.

## Why query callbacks are structs
Thirty-six functions are still bound by hand, and one deliberately is not —
[API coverage](api-coverage.md) lists all of them.

```mermaid
flowchart LR
q["world.Raycast&lt;TCallback&gt;"] --> ctx["context on the stack:<br/>pointer to your struct<br/>+ managed function pointer"]
ctx --> thunk["static thunk<br/>[UnmanagedCallersOnly]"]
thunk --> gen["InvokeRaycast&lt;TCallback&gt;<br/><i>generic, so it can be specialised</i>"]
gen --> your["your OnHit — inlined"]

style q fill:#512BD4,color:#fff
style your fill:#16a34a,color:#fff
style thunk fill:#b09ef5,color:#1a1a1a
```
## The struct layouts are checked against a C compiler

The query is generic over the callback type, so the JIT specialises it and
devirtualises the call: your `OnHit` is inlined into the dispatcher with no
delegate allocation and nothing to keep alive across the transition.
The declarations are generated, but the structs they pass are hand-written
mirrors, and nothing about C# forces a mirror to match. A field of the wrong
width, or two fields swapped, compiles and runs: the call succeeds and reads the
wrong bytes, so a body ends up with its restitution in the friction slot. There
is no crash to investigate.

The one indirection exists because `[UnmanagedCallersOnly]` cannot be applied to
a generic method. The context carries a *managed* function pointer to a generic
helper, which may be generic precisely because it is not the
`UnmanagedCallersOnly` one, and the non-generic native thunk calls through it.
That is one indirect call per hit, against an allocation and a GC handle per
query for the delegate design.
`tools/dump-abi.ps1` compiles a program against the real Box3D headers that
prints `sizeof`, `_Alignof` and `offsetof` for every field, and records the
answers in `abi/native-layout.json`. The test suite holds all 92 structs to that
file — size, every field offset, blittability, and whether a mirror exists at
all — and CI regenerates it, so a submodule bump that moves a field fails the
build instead of shipping.

Measured: a ray cast with a struct callback runs at 163 ns against 167 ns for
the callback-free convenience method, and allocates nothing.
## The layering rule is enforced, not documented

## The build
`Box3D.NET` never names a `Box3D.NET.Native` type in public API. `LayeringTests`
checks that by reflection over the built assembly, because a rule like this
decays quietly — one convenient property and nothing fails.

```mermaid
flowchart LR
sub["external/box3d<br/><i>submodule, pinned, never modified</i>"]
script["tools/build-native.ps1<br/>CMake · shared library"]
runtimes["runtimes/&lt;rid&gt;/native/"]
gen["tools/generate-bindings.ps1"]
generated["Generated/*.g.cs<br/>543 declarations"]
pkg["NuGet packages"]
The sanctioned way down is `Box3D.Interop`, which is a `using` in the consumer's
own source rather than an accident.

sub --> script --> runtimes --> pkg
sub -->|"headers"| gen --> generated --> pkg
## What CI verifies

style sub fill:#d6cdfa,color:#1a1a1a
style pkg fill:#512BD4,color:#fff
```
| | |
| --- | --- |
| Every test, on every supported platform | including determinism, threading, leaks and allocation |
| The packed `.nupkg`, installed into a project that has never heard of this repository | which is the only check that exercises NuGet asset resolution rather than `bin/` |
| The samples, published with NativeAOT | which proves nothing on those paths needs the JIT |
| Generated sources and the ABI dump against the headers | so a submodule bump cannot land silently |
| The public API against the last published package | a break is allowed before 1.0, but it belongs in the changelog |

Box3D is a submodule pinned to a commit and never modified. Both the binding and
the binary are derived from it, which is what makes an upgrade a matter of
moving the submodule, re-running two scripts and reading the diff. CI fails if
the checked-in generated sources differ from what the script produces.
`AllocationTests` is the one worth knowing about before writing code here: it
measures the documented hot paths with `GC.GetAllocatedBytesForCurrentThread`
and requires exactly zero bytes, so a captured closure or a boxed enumerator
fails the build.

The repository README has the commands, the platform matrix and the full list of
test suites.
Loading
Loading