Skip to content
dhannyellPublic

About

Box2D v3.1 for Go — pure Go, idiomatic API, 100% reference API, all 110 official examples, SIMD acceleration, WebGPU/WASM demos, and optional Q32 fixed-point math.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

284 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Box2D V3 Physics Engine for Go

CI Go Reference Latest tag License Go version

dbox2d is an idiomatic, pure Go port of the Box2D v3.1.1 physics engine.

It offers two scalar modes from the same solver: float32 is the default, and -tags dbox2d_fixed selects Q32.32 fixed-point arithmetic. Both modes are deterministic; the difference in their reproducibility contracts is described in Scalar modes and determinism.

Try the browser samples

Open the live demo · fixed mode

The browser demo requires WebGPU and run on a single thread due to limitations in Go's WebAssembly implementation. To use multithreading, try the native version. see the samples.

Highlights

  • Box2D v3.1.1's public API, expressed in Go style.
  • Deterministic results and a checksum over the complete world state.
  • Bodies, shapes, sensors, chains, contacts, seven joint types, queries, casts, continuous collision, events, and debug drawing.
  • Native and WebAssembly samples rendered with WebGPU.
  • Optional SIMD contact solving with scalar-parity checks.

Install

Requires Go 1.26.8 or newer.

go get github.com/dhannyell/dbox2d

The package is named b2, so the import needs no alias and a call reads like the reference: b2Body_GetPosition(bodyId) becomes bodyId.GetPosition(), and b2MakeBox(1, 1) becomes b2.MakeBox(...). Constructors, defaults, and geometry helpers remain package functions.

First world

package main

import "github.com/dhannyell/dbox2d"

func main() {
	def := b2.DefaultWorldDef()
	def.Gravity = b2.V2(0, -10)
	world := b2.CreateWorld(&def)
	defer b2.DestroyWorld(world)

	world.Step(b2.QFromRatio(1, 60), 4)
}

Every scalar of the API is a Q, the scalar of the selected mode. F converts an integer or a float, V2 builds a vector, and Degrees gives an angle in the turns the API uses; see Scalar modes and determinism for the long forms and the rounding contract.

Run the samples

The samples module includes the Box2D scenes in native and WebAssembly hosts. Native runs need cgo, a C toolchain, and a Vulkan, Metal, or D3D12 driver. The browser host needs WebGPU.

SIMD

cd samples
GOTOOLCHAIN=go1.27.0 GOEXPERIMENT=simd go run -tags dbox2d_simd ./cmd/native

To build the browser host:

cd samples
GOTOOLCHAIN=go1.27.0 GOEXPERIMENT=simd CGO_ENABLED=0 GOOS=js GOARCH=wasm go build -tags dbox2d_simd -o web/app.wasm ./cmd/web
go run ./cmd/serve

Scalar

cd samples
go run ./cmd/native
cd samples
CGO_ENABLED=0 GOOS=js GOARCH=wasm go build -o web/app.wasm ./cmd/web
go run ./cmd/serve

Then open http://localhost:8080. The full list of controls, platform requirements, and instructions for adding a scene are in samples/README.md.

Scalar modes and determinism

Both modes are deterministic, but they are different numeric systems and can follow different trajectories. ScalarMode reports the active mode.

Build Arithmetic Best for
default float32 General use, multiplayer, rollback, and authoritative simulation with a controlled build matrix
-tags dbox2d_fixed Signed Q32.32 fixed point A stronger cross-platform reproducibility contract

Float is deterministic for a specific supported build and target. A silently introduced fused multiply-add (FMA), compiler change, or other floating-point variation can make two peers take different paths while each continues to produce internally valid results. For float rollback, lockstep, or authoritative simulation, pin the toolchain and flags, validate every supported target, keep update order and random state stable, and compare checksums regularly.

Fixed point provides a stronger and easier-to-audit contract across toolchains and architectures, but does not make the complete application deterministic by itself.

Use these constructors to create simulation values:

b2.F(3)          // an int, exact
b2.F(0.35)       // a float, rounded once to the mode
b2.F(q)          // a Q, unchanged
b2.V2(1, -2)     // a vector; both arguments share one type
b2.Degrees(90)   // a quarter turn
b2.QZero()
b2.QOne()
b2.QHalf()
b2.QFromInt(3)
b2.QFromRatio(1, 8)
b2.QFromFloat64(0.35)
b2.QMustParse("0.35")

F and V2 are generic over int, float32, float64, and Q, so an untyped literal needs no conversion; F(3) is QFromInt(3) and F(0.35) is QFromFloat64(0.35). Mixed arguments need one type: V2(1, 0.5) does not compile, V2(1.0, 0.5) does.

QFromFloat64 rounds to the nearest scalar of the mode, the same way on every architecture, and QToFloat64 converts back. A constant converts to the same bits everywhere. A float64 computed at run time is only as deterministic as its computation: Go fuses a multiply and an add into one rounding on arm64, and on amd64 with GOAMD64=v3, so the same expression can give different bits in different builds. QMustParse rounds a decimal once, while a literal passed to QFromFloat64 rounds to float64 first, so a long literal can differ in the last bit.

Angles in the API are turns in both modes: one quarter turn is QFromRatio(1, 4), and an angular velocity is in turns per second. Inside, the body state keeps radians per second, and a joint keeps its angles in radians in float mode, as Box2D does, and in turns in fixed mode (D-004).

Build and test fixed mode with:

go build -tags dbox2d_fixed ./...
go test -tags dbox2d_fixed ./...
cd samples
go test -tags dbox2d_fixed ./...
go run -tags dbox2d_fixed ./cmd/native

The published sample page serves the float build by default. Add ?mode=fixed to load the fixed build instead.

The port is checked against traces of the reference compiled without SIMD and without FMA; the collision functions match the reference bit for bit in float mode except at three documented sites, and the fixed mode stays within measured budgets. See DIVERGENCES.md D-018.

Reproducing Box2D bit for bit

With -tags dbox2d_upstream_pairs, float mode reproduces the output bits of the reference built without SIMD and without FMA: every frozen scene trace matches over its full length, in the scalar and the SIMD families alike.

go test -tags dbox2d_upstream_pairs ./...

The tag restores the broadphase pair order of Box2D, which follows the walk of the tree. The default build sorts those pairs instead, so its world does not depend on tree topology, and it parts from the reference wherever the two orders differ (DIVERGENCES.md D-013). CI runs both.

SIMD

Use dbox2d_simd to enable the SIMD contact solver. It is optional and produces the same result bits as the scalar solver.

Target Native path
amd64 AVX2, with scalar fallback when AVX2 is unavailable
arm64 NEON
wasm and other targets generic path

Native vector paths require Go 1.27.0 and GOEXPERIMENT=simd:

GOTOOLCHAIN=go1.27.0 GOEXPERIMENT=simd go build -tags dbox2d_simd ./...
GOTOOLCHAIN=go1.27.0 GOEXPERIMENT=simd go build -tags dbox2d_simd,dbox2d_fixed ./...

The generic path is useful for compatibility and conformance; benchmark it before enabling it for production.

Workers, callbacks, and drawing

WorldDef.WorkerCount controls native workers without changing result bits. WebAssembly always steps on one worker. Go callbacks use closures rather than the C API's void* context.

DefaultDebugDraw provides no-op callbacks. Implement the operations your host needs and call worldId.Draw(&draw); the library walks the world and the host decides how to render it.

Compatibility with Box2D

This is a port, not a new physics design. It preserves the upstream file structure, names without the b2 prefix, and order of operations so code can be compared with its Box2D counterpart.

It does not promise the same output bits as Box2D. Both scalar modes share one source, and the fixed-point mode forbids some float-oriented operations, so those operations change in both modes. Each intentional change is documented with its rationale and test coverage in DIVERGENCES.md.

dbox2d is not affiliated with, endorsed by, or supported by the Box2D project. Please report dbox2d issues here rather than upstream.

Performance

Performance depends on the workload and target. Native SIMD improves contact-heavy scenes on supported CPUs, while the generic path may be slower than scalar. Measure both scalar modes and the target that matters to you:

go test -run "^$" -bench . -benchmem
go test -tags dbox2d_fixed -run "^$" -bench . -benchmem

The repository's benchmark results are historical measurements, not a promise for another machine or scene. The saturation counter used by some tests is diagnostic only and never changes simulation results.

Against Box2D

The seven benchmark scenes of the reference run on both sides under one protocol: the same step counts, the same AVX2 lane path at width 8, fused multiply-add disabled in the C build because the port never emits it, and validation off in both. Solver milliseconds are the sum of the per-step minima, and each figure is a geometric mean over the seven scenes.

Workers Port over reference With PGO Reference speedup Port speedup Reference efficiency Port efficiency
1 1.35x 1.30x 1.00x 1.00x 100% 100%
2 1.29x 1.26x 1.73x 1.82x 87% 91%
4 1.24x 1.20x 2.78x 3.03x 70% 76%
8 1.29x 1.24x 3.99x 4.18x 50% 52%

The port scales slightly better than the reference at every worker count, so the gap is single-thread work rather than the executor. The per-scene ratios at one worker run 1.21x to 1.44x; at eight they run 1.19x to 1.39x, with many_pyramids the slowest scene on both counts.

The port also lands on the same result bits at every worker count, in all seven scenes. The reference does so in six; its rain hash changes with the worker count, reproducibly.

Measured on an AMD Ryzen 7 5800X3D, Windows/amd64, sixteen logical cores, GCC 13.2.0, Go 1.27.0, three repeats.

Running the comparison

The harness is in the tree, so the numbers above can be checked rather than taken. The reference is not vendored: clone Box2D v3.1.1 beside this repository, or pass -DBOX2D_SOURCE_DIR=<path>.

git clone --branch v3.1.1 --depth 1 https://github.com/erincatto/box2d ../box2d
cmake -S tools/cbench -B build/cbench -G Ninja -DCMAKE_BUILD_TYPE=Release
cmake --build build/cbench

Run one arm at a time and write a JSON file per worker count:

mkdir -p testdata/bench
./build/cbench/cbench --want-lane=avx2 --repeats=3 --workers=8 --arm=c-avx2 --out=testdata/bench/c-avx2-w8.json
GOTOOLCHAIN=go1.27.0 GOEXPERIMENT=simd go test -tags dbox2d_simd -run TestSceneBench -timeout 7200s . -bench.arm=go-avx2 -bench.want-lane=avx2 -bench.repeats=3 -bench.workers=8 -bench.out=testdata/bench/go-avx2-w8.json

benchcmp compares one worker count and benchscale a whole sweep. Both refuse to compare runs that did not share a lane path, a lane width, a clock or an observed worker count, so a mismatched pair is an error rather than a misleading ratio:

go run ./tools/benchcmp testdata/bench/c-avx2-w8.json testdata/bench/go-avx2-w8.json
go run ./tools/benchscale -base=c-avx2 testdata/bench/c-avx2-w*.json testdata/bench/go-avx2-w*.json

The protocol, the worker-pool contract, the lane and clock guards, and the per-scene tables are documented in tools/cbench/README.md. Results are not tracked in the repository: one is a single machine on a single day.

Profile-guided optimization

A profile takes 3 to 6 percent off the solver: 2.7 percent on one worker and 5 to 6 percent from two workers up, in all seven scenes. It changes no result bits. The checksum witnesses, the conformance traces under dbox2d_upstream_pairs, the no-FMA gate and the per-scene hashes all hold with a profile applied, because every float product already sits inside an explicit rounding conversion, and a fused multiply-add cannot cross that barrier.

Go reads default.pgo from the directory of the main package, so a library cannot ship one for you. Collect a profile of your own workload, which is the one whose hot paths matter:

go test -run "^$" -bench . -cpuprofile=cpu.pprof
cp cpu.pprof ./cmd/your_app/default.pgo
go build ./cmd/your_app

A profile kept elsewhere works through the flag, and -pgo=off builds without one:

go build -pgo=cpu.pprof ./cmd/game
go build -pgo=off ./cmd/game

The sample hosts carry a profile of the benchmark scenes, so go run ./cmd/native and the WebAssembly build use it with no flag.

License

MIT. This is a derivative work of Box2D, which is also MIT. See LICENSE.

About

Box2D v3.1 for Go — pure Go, idiomatic API, 100% reference API, all 110 official examples, SIMD acceleration, WebGPU/WASM demos, and optional Q32 fixed-point math.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages