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
2 changes: 2 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Preserve upstream document bytes and original whitespace on every platform.
upstream/** -text -whitespace
145 changes: 145 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
name: CI

on:
push:
branches: [main]
pull_request:
branches: [main]
workflow_dispatch:

permissions:
contents: read

concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true

jobs:
mcpp:
name: mcpp (${{ matrix.os }}, ${{ matrix.toolchain }})
strategy:
fail-fast: false
matrix:
include:
- { os: ubuntu-24.04, toolchain: 'gcc@16.1.0' }
- { os: ubuntu-24.04, toolchain: 'llvm@23.1.3' }
- { os: ubuntu-24.04-arm, toolchain: 'gcc@16.1.0' }
- { os: ubuntu-24.04-arm, toolchain: 'llvm@23.1.3' }
- { os: macos-15, toolchain: 'llvm@23.1.3' }
- { os: windows-2025, toolchain: 'llvm@23.1.3' }
runs-on: ${{ matrix.os }}
timeout-minutes: 60
defaults:
run:
shell: bash
env:
XLINGS_NON_INTERACTIVE: 1
steps:
- uses: actions/checkout@v4

- uses: actions/setup-python@v5
with:
python-version: "3.12"

- name: Mwrap identity and bilingual README contract
run: python3 tools/check_mwrap_release.py

- name: Install Xlings (Linux/macOS)
if: runner.os != 'Windows'
run: |
curl -fsSL https://raw.githubusercontent.com/openxlings/xlings/refs/heads/main/tools/other/quick_install.sh | bash
echo "$HOME/.xlings/subos/current/bin" >> "$GITHUB_PATH"

- name: Install Xlings (Windows)
if: runner.os == 'Windows'
shell: pwsh
run: |
irm https://raw.githubusercontent.com/openxlings/xlings/main/tools/other/quick_install.ps1 | iex
"$env:USERPROFILE\.xlings\subos\current\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append

- name: Install project tools (.xlings.json)
run: |
for attempt in 1 2 3; do
xlings update > /dev/null 2>&1 || true
if xlings install; then break; fi
[ "$attempt" = 3 ] && exit 1
sleep 30
done
mcpp --version

- name: Select the toolchain
run: |
name="${{ matrix.toolchain }}"
mcpp toolchain install "${name%@*}" "${name#*@}"
mcpp toolchain default "$name"

# Until mcpp-index publishes compat.box2d (mcpplibs/mcpp-index#611),
# the compat namespace of this checkout's manifests resolves from that
# PR's branch, as an unchanged namespace view of its descriptors. Once
# the published index has the descriptor, this step does nothing.
- name: Candidate compat index (only while compat.box2d is unpublished)
run: python3 tools/candidate_index.py --ci

- name: Build the modules
run: mcpp build

- name: Tests (the API, every macro and constant mapping against the header, header mixing, determinism)
run: mcpp test

# Optimized (GCC contracts a*b+c into an FMA, if allowed to, only then),
# and with NDEBUG (b2InternalAssert is not declared).
- name: Tests, release profile
run: mcpp test --profile release

- name: Export audit (the generated sources are fresh; every declaration and macro of the public headers)
run: python3 tools/check_exports.py

# The line this row is compared with every other row by: the same sleep
# step and hashes on every platform, compiler and worker count.
- name: Determinism (examples/box2d, dev and release, every worker count and SIMD path, against the pinned hashes)
run: python3 tools/run_determinism.py --toolchain "${{ matrix.toolchain }}"

# The Web: wasm32-emscripten (emsdk from xlings, through mcpp), the tests and
# the determinism runs under node, with the same pinned hashes.
web:
name: mcpp (web, wasm32-emscripten)
runs-on: ubuntu-24.04
timeout-minutes: 60
defaults:
run:
shell: bash
env:
XLINGS_NON_INTERACTIVE: 1
steps:
- uses: actions/checkout@v4

- uses: actions/setup-python@v5
with:
python-version: "3.12"

- name: Install Xlings
run: |
curl -fsSL https://raw.githubusercontent.com/openxlings/xlings/refs/heads/main/tools/other/quick_install.sh | bash
echo "$HOME/.xlings/subos/current/bin" >> "$GITHUB_PATH"

- name: Install project tools (.xlings.json)
run: |
for attempt in 1 2 3; do
xlings update > /dev/null 2>&1 || true
if xlings install; then break; fi
[ "$attempt" = 3 ] && exit 1
sleep 30
done
mcpp --version

- name: Candidate compat index (only while compat.box2d is unpublished)
run: python3 tools/candidate_index.py --ci

- name: Tests under node
run: mcpp test --target wasm32-emscripten

- name: Tests under node, release profile
run: mcpp test --target wasm32-emscripten --profile release

- name: Determinism under node (against the pinned hashes)
run: python3 tools/run_determinism.py --target wasm32-emscripten
49 changes: 49 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
name: Release
on:
push:
tags: ["mwrap-*"]
permissions:
contents: read
jobs:
publish:
runs-on: ubuntu-latest
timeout-minutes: 30
permissions:
contents: write
actions: read
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Check tag, upstream version and bilingual entry pages
run: python3 tools/check_mwrap_release.py --tag "$GITHUB_REF_NAME"
- name: Require successful source CI at this exact commit
env:
GH_TOKEN: ${{ github.token }}
shell: bash
run: |
set -euo pipefail
result=$(gh run list --repo "$GITHUB_REPOSITORY" --workflow ci.yml --commit "$GITHUB_SHA" --limit 1 --json status,conclusion --jq '.[0] | .status == "completed" and .conclusion == "success"')
test "$result" = true || { echo 'No successful source CI at the tag commit'; exit 1; }
- name: Build immutable source asset and publish
env:
GH_TOKEN: ${{ github.token }}
shell: bash
run: |
set -euo pipefail
version="${GITHUB_REF_NAME#mwrap-}"
repo="${GITHUB_REPOSITORY#*/}"
asset="${repo}-${version}.tar.gz"
git archive --format=tar.gz --prefix="${repo}-${version}/" "$GITHUB_SHA" > "$RUNNER_TEMP/$asset"
(cd "$RUNNER_TEMP" && sha256sum "$asset" > "$asset.sha256")
if gh release view "$GITHUB_REF_NAME" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then
echo 'Release already exists; refusing to change immutable assets'; exit 1
fi
extra=()
if [[ "$version" == *-rc* ]]; then extra+=(--prerelease); fi
gh release create "$GITHUB_REF_NAME" "$RUNNER_TEMP/$asset" "$RUNNER_TEMP/$asset.sha256" \
--repo "$GITHUB_REPOSITORY" --verify-tag --title "$repo $version" \
--notes "Module wrapper for upstream $version. New mwrap package identities become installable after mcpp-index publication; see the bilingual README for the tested support scope." "${extra[@]}"
33 changes: 33 additions & 0 deletions .github/workflows/templates.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
name: Actual project templates
on:
push:
branches: [main]
pull_request:
workflow_dispatch:
permissions:
contents: read
concurrency:
group: templates-${{ github.ref }}
cancel-in-progress: true
env:
MCPP_VERSION: "2026.10.26.1"
jobs:
templates:
runs-on: ubuntu-24.04
timeout-minutes: 60
steps:
- uses: actions/checkout@v4
- name: Install the pinned mcpp
shell: bash
run: |
archive="mcpp-${MCPP_VERSION}-linux-x86_64.tar.gz"
curl -L -fsS -o "$archive" "https://github.com/mcpp-community/mcpp/releases/download/v${MCPP_VERSION}/$archive"
tar -xzf "$archive"
echo "$PWD/mcpp-${MCPP_VERSION}-linux-x86_64/bin" >> "$GITHUB_PATH"
# The candidate index (tools/candidate_index.py) only while mcpp-index
# does not publish compat.box2d.
- name: Actual scaffold, strict build and run of the default template
shell: bash
run: |
index=$(python3 tools/candidate_index.py --checkout-only)
python3 tools/test_template.py --toolchain gcc@16.1.0 ${index:+--index "$index"}
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
target
compile_commands.json
mcpp.lock
.mcpp/
.xlings/
doctor.log
__pycache__/
5 changes: 5 additions & 0 deletions .xlings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"workspace": {
"xim:mcpp": "2026.10.26.1"
}
}
18 changes: 18 additions & 0 deletions NOTICE
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
box2d-m: C++ module wrapper
Copyright (c) 2026 mcpp-community contributors

The wrapper contributions are licensed under the Apache License, Version 2.0.
Upstream code, copied interfaces and dependencies retain their original
licenses and copyright notices. Box2D is distributed under the MIT License:

Copyright (c) 2022 Erin Catto

Its original README and license are retained in upstream/box2d/. The library
itself is built by compat.box2d from the same release; this wrapper links it,
it does not modify it.

The determinism scene of tests/determinism_test.cpp and examples/box2d is
upstream's "falling hinges" (shared/determinism.c, MIT), rewritten in C++
against the module.

Provenance and checksums are recorded in upstream/documents.json.
123 changes: 122 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,122 @@
# box2d-m
# box2d

> Box2D v3, the 2D physics engine for games, through C++23 modules.

**English** · [简体中文](README.zh.md) · [Upstream](https://github.com/erincatto/box2d) · [Releases](https://github.com/mcpplibs/box2d-m/releases) · [mcpp](https://github.com/mcpp-community/mcpp) · [Index](https://github.com/mcpplibs/mcpp-index)

[![C++23](https://img.shields.io/badge/C%2B%2B-23-blue.svg)](https://en.cppreference.com/w/cpp/23)
[![License](https://img.shields.io/badge/license-Apache--2.0-blue.svg)](LICENSE)
[![Box2D](https://img.shields.io/badge/Box2D-MIT-blue.svg)](upstream/box2d/LICENSE)
[![mcpp-community](https://img.shields.io/badge/mcpp--community-mwrap-blue)](https://github.com/mcpp-community)
[![CI](https://github.com/mcpplibs/box2d-m/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/mcpplibs/box2d-m/actions/workflows/ci.yml)

Box2D 3.2.0 simulates rigid bodies in 2D: bodies, shapes, joints, contact and
sensor events, ray and shape casts, continuous collision and a built-in task
scheduler, in MIT-licensed C17. The upstream C API is unchanged:
`b2CreateWorld`, `b2World_Step`, `b2CreatePolygonShape` and `b2MakeRot` are
the upstream globals of `<box2d/box2d.h>`. The library is `compat.box2d`
3.2.0, built as upstream builds it for determinism (`-ffp-contract=off`, its
default SIMD paths). Box2D is cross-platform deterministic, and every CI run
checks it: upstream's own determinism scene and a struck 20-row pyramid give
the same bits on Linux x86_64 (GCC, LLVM), Linux arm64 (GCC, LLVM), macOS,
Windows and wasm32, with 1 to 8 workers and either SIMD width.

## Modules

- `import mcpplibs.mwrap.box2d;`: the whole API of `<box2d/box2d.h>` (worlds, bodies, shapes, chains, joints, events, queries, collision and geometry, the inline math, the vector operators), the headers' static constants (`b2_nullBodyId`, `b2Rot_identity` …) in `mcpplibs::mwrap::box2d`, re-exporting the module below
- `import mcpplibs.mwrap.box2d.macros;`: `B2_PI`, `B2_MAX_POLYGON_VERTICES`, `B2_HASH_INIT`, `B2_LINEAR_SLOP()`, `B2_ID_EQUALS` … in `mcpplibs::mwrap::box2d::macros`

> More Modules API -> [Usage and API](docs/usage.md)

## Getting started

```sh
mcpp new tower --template mcpplibs.mwrap.box2d@3.2.0
cd tower
mcpp run
```

The default template knocks a tower of boxes over with a ball on a fixed
60 Hz step, prints the boxes as they fall, and ends with the hash of the
final state: the same on every platform.

```toml
[dependencies]
"mcpplibs.mwrap.box2d" = "3.2.0"
```

```cpp
import std;
import mcpplibs.mwrap.box2d;

int main() {
b2WorldDef worldDef = b2DefaultWorldDef(); // gravity (0, -10)
b2WorldId world = b2CreateWorld(&worldDef);

b2BodyDef groundDef = b2DefaultBodyDef();
groundDef.position = b2Vec2{0.0f, -1.0f};
b2BodyId ground = b2CreateBody(world, &groundDef);
b2Polygon groundBox = b2MakeBox(20.0f, 1.0f);
b2ShapeDef shapeDef = b2DefaultShapeDef();
b2CreatePolygonShape(ground, &shapeDef, &groundBox);

b2BodyDef boxDef = b2DefaultBodyDef();
boxDef.type = b2_dynamicBody;
boxDef.position = b2Vec2{0.0f, 4.0f};
b2BodyId box = b2CreateBody(world, &boxDef);
b2Polygon square = b2MakeBox(0.5f, 0.5f);
b2CreatePolygonShape(box, &shapeDef, &square);

for (int i = 0; i < 90; ++i) b2World_Step(world, 1.0f / 60.0f, 4);
b2Vec2 p = b2Body_GetPosition(box);
std::println("the box rests at ({:.2f}, {:.2f})", p.x, p.y); // (0.00, 0.50)
b2DestroyWorld(world);
}
```

## Determinism

Box2D 3 gives the same result for the same inputs on every platform it
supports, provided the floating point is built as upstream builds it:
without contracting `a*b+c` into an FMA. `compat.box2d` builds the library
that way. Your own float arithmetic that feeds the simulation (positions you
compute, `b2MakeRot` and the other inline math, which compile into your code)
needs the same flag where FMA exists (arm64 always, x86_64 with AVX2/FMA):
`[build] cxxflags = ["-ffp-contract=off"]`, as this repository's tests and
examples use. [tools/run_determinism.py](tools/run_determinism.py) and
[examples/box2d](examples/box2d) (`box2d-determinism`) report the hashes on
any machine.

## Macros

Constants such as `B2_PI` and `B2_MAX_POLYGON_VERTICES` keep the type their
macro has. `B2_HUGE`, `B2_LINEAR_SLOP` and the other values that read
`b2_lengthUnitsPerMeter` are functions (`B2_LINEAR_SLOP()`), because
`b2SetLengthUnitsPerMeter` changes them at run time. `B2_IS_NULL`,
`B2_IS_NON_NULL` and `B2_ID_EQUALS` are constexpr functions whose body is the
macro. The declaration specifiers (`B2_API`, `B2_INLINE` …), the initializers
`B2_NULL_ID` / `B2_ZERO_INIT` and the assertions `B2_ASSERT` / `B2_VALIDATE`
stay macros in `<mwrap/box2d/macros.hpp>`, included before the import.
Details: [docs/usage.md](docs/usage.md).

## Examples

- [examples/box2d](examples/box2d): `box2d-determinism`, which runs the determinism scenes under a chosen
worker count and SIMD path and prints the sleep step and the hashes, to compare machines and compilers.

## Build and testing

CI on Linux (x86_64 GCC and LLVM, arm64 GCC and LLVM), macOS and Windows
builds the modules and runs the tests, the export audit
([tools/check_exports.py](tools/check_exports.py)) and the determinism runs;
a Web job runs the tests and the determinism runs as wasm32-emscripten under
node. The audit checks that the generated sources are fresh, that every
declaration is exported and that every macro is mapped or listed with its
reason. A separate job scaffolds the default template as an installed package
and runs it.

## License

[Apache-2.0](LICENSE) for the wrapper, by mcpp-community ([NOTICE](NOTICE)).
Box2D is MIT ([upstream/box2d/LICENSE](upstream/box2d/LICENSE)); its license
and README are deployed beside every program that depends on this package.
Loading
Loading