diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..8a6c241 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Preserve upstream document bytes and original whitespace on every platform. +upstream/** -text -whitespace diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..66b13f8 --- /dev/null +++ b/.github/workflows/ci.yml @@ -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 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..f0c6447 --- /dev/null +++ b/.github/workflows/release.yml @@ -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[@]}" diff --git a/.github/workflows/templates.yml b/.github/workflows/templates.yml new file mode 100644 index 0000000..5fb834b --- /dev/null +++ b/.github/workflows/templates.yml @@ -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"} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b6dfbae --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +target +compile_commands.json +mcpp.lock +.mcpp/ +.xlings/ +doctor.log +__pycache__/ diff --git a/.xlings.json b/.xlings.json new file mode 100644 index 0000000..730a8ff --- /dev/null +++ b/.xlings.json @@ -0,0 +1,5 @@ +{ + "workspace": { + "xim:mcpp": "2026.10.26.1" + } +} diff --git a/NOTICE b/NOTICE new file mode 100644 index 0000000..b1fbae3 --- /dev/null +++ b/NOTICE @@ -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. diff --git a/README.md b/README.md index 38b4038..1c3a73b 100644 --- a/README.md +++ b/README.md @@ -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 ``. 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 `` (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 ``, 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. diff --git a/README.zh.md b/README.zh.md new file mode 100644 index 0000000..975151d --- /dev/null +++ b/README.zh.md @@ -0,0 +1,112 @@ +# box2d + +> 以 C++23 模块使用 Box2D v3——游戏用 2D 物理引擎。 + +[English](README.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 模拟二维刚体:刚体、形状、关节、接触与传感器事件、射线与形状投射、 +连续碰撞和内置的任务调度器,以 MIT 许可的 C17 编写。上游 C API 保持不变: +`b2CreateWorld`、`b2World_Step`、`b2CreatePolygonShape`、`b2MakeRot` 都是 +`` 的上游全局名字。库本身是 `compat.box2d` 3.2.0,按上游为确定性 +所做的方式构建(`-ffp-contract=off`、上游默认的 SIMD 路径)。Box2D 跨平台确定, +每次 CI 都会检查:上游自己的确定性场景和一个被撞倒的 20 层金字塔,在 Linux x86_64 +(GCC、LLVM)、Linux arm64(GCC、LLVM)、macOS、Windows 和 wasm32 上,用 1 到 8 个 +worker、两种 SIMD 宽度,得到逐位相同的结果。 + +## 模块 + +- `import mcpplibs.mwrap.box2d;`:`` 的完整 API(世界、刚体、形状、链、关节、事件、查询、碰撞与几何、内联数学函数、向量运算符),头文件的静态常量(`b2_nullBodyId`、`b2Rot_identity` 等)位于 `mcpplibs::mwrap::box2d`,并重新导出下面的模块 +- `import mcpplibs.mwrap.box2d.macros;`:`B2_PI`、`B2_MAX_POLYGON_VERTICES`、`B2_HASH_INIT`、`B2_LINEAR_SLOP()`、`B2_ID_EQUALS` 等,位于 `mcpplibs::mwrap::box2d::macros` + +> 更多模块 API -> [用法与 API](docs/usage.md) + +## 快速开始 + +```sh +mcpp new tower --template mcpplibs.mwrap.box2d@3.2.0 +cd tower +mcpp run +``` + +默认模板以固定的 60 Hz 步长用一个球撞倒一座箱子塔,打印箱子下落的位置,最后输出 +终态的哈希:在每个平台上都相同。 + +```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); +} +``` + + +## 确定性 + +Box2D 3 在它支持的每个平台上,对相同的输入给出相同的结果,前提是浮点按上游的方式 +构建:不把 `a*b+c` 收缩为 FMA。`compat.box2d` 就是这样构建库的。你自己参与模拟的 +浮点运算(自己算的位置、`b2MakeRot` 等会编译进你代码里的内联数学函数),在有 FMA 的 +地方(arm64 总有;x86_64 在 AVX2/FMA 下)也需要同样的选项: +`[build] cxxflags = ["-ffp-contract=off"]`,本仓库的测试和示例就是这样做的。 +[tools/run_determinism.py](tools/run_determinism.py) 和 [examples/box2d](examples/box2d) +(`box2d-determinism`)可在任何机器上报告这些哈希。 + +## 宏 + +`B2_PI`、`B2_MAX_POLYGON_VERTICES` 等常量保持其宏的类型。`B2_HUGE`、`B2_LINEAR_SLOP` +等读取 `b2_lengthUnitsPerMeter` 的值是函数(`B2_LINEAR_SLOP()`),因为 +`b2SetLengthUnitsPerMeter` 会在运行时改变它们。`B2_IS_NULL`、`B2_IS_NON_NULL`、 +`B2_ID_EQUALS` 是函数体就是宏本身的 constexpr 函数。声明说明符(`B2_API`、 +`B2_INLINE` 等)、初始化器 `B2_NULL_ID` / `B2_ZERO_INIT` 以及断言 `B2_ASSERT` / +`B2_VALIDATE` 保留为宏,在 `` 中,于 import 之前包含。 +详见 [docs/usage.md](docs/usage.md)。 + +## 示例 + +- [examples/box2d](examples/box2d):`box2d-determinism`,在指定的 worker 数和 SIMD 路径下运行确定性场景, + 打印休眠步数和哈希,用来比较不同机器与编译器。 + +## 构建与测试 + +Linux(x86_64 GCC 与 LLVM、arm64 GCC 与 LLVM)、macOS 和 Windows 的 CI 构建模块, +运行测试、导出审计([tools/check_exports.py](tools/check_exports.py))和确定性检查; +Web 任务以 wasm32-emscripten 在 node 下运行测试和确定性检查。审计检查生成的源文件是否 +最新、每个声明是否都已导出、每个宏是否已映射或列明原因。另有一个任务把默认模板作为 +已安装的包生成项目并运行。 + +## 许可 + +包装代码为 [Apache-2.0](LICENSE),由 mcpp-community 提供([NOTICE](NOTICE))。 +Box2D 为 MIT([upstream/box2d/LICENSE](upstream/box2d/LICENSE));它的许可证和 README +会部署在每个依赖本包的程序旁边。 diff --git a/docs/usage.md b/docs/usage.md new file mode 100644 index 0000000..c42e7e3 --- /dev/null +++ b/docs/usage.md @@ -0,0 +1,76 @@ +# Using mcpplibs.mwrap.box2d + +`import mcpplibs.mwrap.box2d;` declares Box2D 3.2.0's API as `` does. Types, enumerators, functions, the inline math and the vector operators are the upstream globals, the very same entities as the header's, so a translation unit may include the header too. The library is `compat.box2d` 3.2.0: a static library in single precision, built as upstream's CMake builds it (`-std=gnu17`, `-ffp-contract=off`, SSE2 with the AVX2 contact solver dispatched at run time on x86_64, Neon on arm64, `-msimd128 -msse2` on the Web). + +## Modules + +| Module | What it holds | +|---|---| +| `mcpplibs.mwrap.box2d` | the 110 types (structs such as `b2WorldDef`, `b2BodyDef`, `b2ShapeDef`, `b2Polygon`, `b2Manifold`, the ids `b2WorldId` … `b2ContactId`, the enums `b2BodyType`, `b2ShapeType`, `b2JointType`, `b2HexColor` …, the callback function types, the aliases `b2Pos` and `b2WorldTransform`), 3 opaque structs (`b2DynamicTree`, `b2Recording`, `b2Replay`), all 176 enumerators, all 506 `B2_API` functions, the 91 inline functions (`b2MakeRot`, `b2Add`, `b2TransformPoint`, `b2StoreBodyId` …), the 8 C++ vector operators and `b2_lengthUnitsPerMeter`; the 13 static constants in `mcpplibs::mwrap::box2d`; re-exports the module below | +| `mcpplibs.mwrap.box2d.macros` | the macros, in `mcpplibs::mwrap::box2d::macros` | + +The public headers are the ones upstream installs (`BOX2D_API_FILES` in src/CMakeLists.txt): `base.h`, `box2d.h`, `collision.h`, `config.h`, `constants.h`, `id.h`, `math_functions.h`, `math_types.h`, `types.h`. + +### Adapted: the static constants + +The headers define `b2Vec2_zero`, `b2Rot_identity`, `b2Transform_identity`, `b2Mat22_zero`, `b2Pos_zero`, `b2WorldTransform_identity`, `b2_emptySimplexCache` and the null ids `b2_nullWorldId`, `b2_nullBodyId`, `b2_nullShapeId`, `b2_nullChainId`, `b2_nullJointId`, `b2_nullContactId` as `static const` objects: internal linkage, which a named module cannot export. Each is an `inline constexpr` of the same type and initializer in `mcpplibs::mwrap::box2d`, under the same name, checked byte for byte against the header's in [tests/macros_mix_test.cpp](../tests/macros_mix_test.cpp). `using namespace mcpplibs::mwrap::box2d;` makes them read as they do with the header. With the header included too, its constants are its own (`::b2_nullBodyId`); do not also bring the namespace in with a using-directive there, or the unqualified names are ambiguous. + +The inline functions need no adaptation: `B2_INLINE`, `B2_FORCE_INLINE` and `B2_ID_INLINE` are `inline` (external linkage) in C++, so they are exported as they are. + +### Conditional: b2InternalAssert + +`b2InternalAssert`, which `B2_ASSERT` calls, is declared by `base.h` and defined by the library only when `NDEBUG` is not defined (or `B2_ENABLE_ASSERT` is); the module exports it under the same condition. + +### Determinism + +Box2D 3 computes the same bits for the same inputs on every platform it supports, with any worker count and SIMD width, provided the floating point is compiled without contracting `a*b+c` into an FMA. `compat.box2d` compiles the library with `-ffp-contract=off`. Code of your own whose float arithmetic feeds the simulation (positions, velocities, `b2MakeRot` and the other inline math, which compile into your translation units) needs the flag too wherever an FMA exists (arm64 always; x86_64 under AVX2/FMA): `[build] cxxflags = ["-ffp-contract=off"]`. This package compiles its module and tests with it. Values that are exact in binary (integers, halves) are the same either way. + +[tests/determinism_test.cpp](../tests/determinism_test.cpp) pins two scenes, run with 1, 2, 4 and 8 workers of the built-in scheduler, 4 workers of a user task system, and the SSE2 fallback with 1 and 4 workers: + +| Scene | Sleep step | Transform hash | Event hash | `b2World_GetStateHash` | +|---|---|---|---|---| +| falling hinges (upstream's `shared/determinism.c`) | 299 | `0x465F3DF4` (upstream's pin) | `0xD019CCFF` | `0x8BD019D8675ACE67` | +| 20-row pyramid struck by a ball, 600 steps, sleep off | — | `0x790D7C28` | `0xE8B80415` | `0xE4E1D89FFCE4C43E` | + +Every CI row computes these values: Linux x86_64 (GCC 16.1, LLVM 23.1.3), Linux arm64 (GCC 16.1, LLVM 23.1.3), macOS arm64, Windows x86_64 and wasm32-emscripten under node. [tools/run_determinism.py](../tools/run_determinism.py) runs the same configurations through [examples/box2d](../examples/box2d) and prints one line per scene. + +## Macros + +A named module cannot export a macro. `mcpplibs::mwrap::box2d::macros` holds the ones a C++ entity can be, under their original names. + +- **Constants** as `inline constexpr`, each of the type its macro expands to: `B2_PI`, `B2_MAX_ROTATION`, `B2_CONTACT_RECYCLE_COS_ANGLE`, `B2_AABB_MARGIN_FRACTION`, `B2_TIME_TO_SLEEP` are `float`; `B2_NULL_INDEX`, `B2_ENABLE_VALIDATION`, `B2_VERSION_MAJOR` / `_MINOR` / `_REVISION`, `B2_HASH_INIT`, `B2_MAX_POLYGON_VERTICES`, `B2_MAX_WORKERS`, `B2_MAX_TASKS`, `B2_GRAPH_COLOR_COUNT`, `B2_MAX_WORLDS`, `B2_NAME_LENGTH`, `B2_MAX_RESTITUTION_ITERATIONS`, `B2_DEFAULT_CATEGORY_BITS` are `int`; `B2_DEFAULT_MASK_BITS` is `uint64_t` (`UINT64_MAX`). `B2_MAX_WORLDS` and `B2_NAME_LENGTH` are upstream's defaults, which the library is built with. +- **Length-unit values** as functions of no arguments: `B2_HUGE()`, `B2_LINEAR_SLOP()`, `B2_SPECULATIVE_DISTANCE()`, `B2_CONTACT_RECYCLE_DISTANCE()`, `B2_MAX_AABB_MARGIN()`. Their macros read `b2_lengthUnitsPerMeter`, which `b2SetLengthUnitsPerMeter` changes at run time, so they are not constants; each function returns the macro's value, of its type (`float`), at the call. +- **Computations** as `constexpr` function templates: `B2_IS_NULL`, `B2_IS_NON_NULL`, `B2_ID_EQUALS`. Each body is the macro itself, expanded on the parameters: the result has the macro's value and type (`bool`) for every id type. Each argument is evaluated once, before the call (`B2_ID_EQUALS` evaluates each of its arguments three times). + +Each mapping is checked against the raw header — the constants for type and value, the length-unit values at two length units, the computations for null and non-null ids of every id type — in [tests/macros_mix_test.cpp](../tests/macros_mix_test.cpp). + +### What stays a macro + +These stay macros in ``, which is ``: + +| Macros | Why | +|---|---| +| `BOX2D_EXPORT`, `B2_API`, `B2_INLINE`, `B2_FORCE_INLINE`, `B2_ID_INLINE` | declaration specifiers (`BOX2D_EXPORT` is empty here: the library is static) | +| `B2_ALIGN_AS(N)` | an alignment specifier | +| `B2_LITERAL(T)`, `B2_ZERO_INIT`, `B2_NULL_ID` | initializer syntax: C compound literals, `{}` for any type | +| `B2_ASSERT`, `B2_VALIDATE`, `B2_BREAKPOINT` | assertions: they stringify the condition and report the call site's `__FILE__` / `__LINE__`, or trap | + +Include the side header before the import. While a header macro is defined, it expands even inside a qualified name: an object-like macro therefore hides `macros::B2_PI`; use the plain macro, or set it aside with `#pragma push_macro`. A function-like macro does not expand when its name is followed by `)`, so `(macros::B2_ID_EQUALS)(a, b)` calls the function beside the header ([tests/header_mix_test.cpp](../tests/header_mix_test.cpp)). [tools/check_exports.py](../tools/check_exports.py) accounts for all 40 macros. + +## Licensing + +The wrapper's code is Apache-2.0. Box2D is MIT ([upstream/box2d/LICENSE](../upstream/box2d/LICENSE)); a program built with it, statically linked, carries the MIT notice. + +`runtime.deploy` copies the wrapper's LICENSE and NOTICE and Box2D's README and LICENSE into `licenses/box2d/` beside every program that depends on this package. + +## Checks + +- `mcpp test` runs five tests: + - [tests/api_test.cpp](../tests/api_test.cpp): the API through the module only — the version, world definitions, a box that falls, rests and sleeps, null ids and id storage, polygon, circle, capsule and chain shapes, ray casts and AABB overlaps, a revolute joint, sensor events, the custom filter callback, hulls, mass, manifolds, segment distance and ray casts against shapes, the inline math and operators, the length unit, a custom allocator and a user task system; + - [tests/macros_mix_test.cpp](../tests/macros_mix_test.cpp) (generated): every constant, length-unit value, function and static constant against the header; + - [tests/header_mix_test.cpp](../tests/header_mix_test.cpp): the header beside the import — the same entities, `B2_API`, `B2_INLINE`, `B2_LITERAL` and `B2_ALIGN_AS` on declarations of one's own, `B2_NULL_ID`, `B2_ZERO_INIT` and `B2_ASSERT` through the assert callback; + - [tests/macros_test.cpp](../tests/macros_test.cpp): the macros module alone, at compile time; + - [tests/determinism_test.cpp](../tests/determinism_test.cpp): the two scenes above under every configuration, against the pinned values. +- `python3 tools/check_exports.py` checks that the sources are what [tools/gen_exports.py](../tools/gen_exports.py) writes from the pinned headers, and that every declaration is exported, every static constant adapted, and every macro mapped or kept. +- `python3 tools/run_determinism.py` builds [examples/box2d](../examples/box2d) in the dev and the release profile (optimized: GCC contracts `a*b+c` into an FMA only when optimizing, if allowed; Clang at every level) and compares every configuration's result with the pins; `mcpp test --profile release` runs the tests optimized and with `NDEBUG`. +- `python3 tools/test_template.py` scaffolds the default template from an installed copy of the package, then builds and runs it. diff --git a/examples/box2d/mcpp.toml b/examples/box2d/mcpp.toml new file mode 100644 index 0000000..f7392fc --- /dev/null +++ b/examples/box2d/mcpp.toml @@ -0,0 +1,17 @@ +[package] +name = "box2d" +version = "0.1.0" +description = "box2d-determinism, written against import mcpplibs.mwrap.box2d: runs Box2D's determinism scenes (upstream's falling hinges, a struck 20-row pyramid) under a chosen worker count and SIMD path and reports the sleep step and the hashes of the final state, to compare one machine, compiler or thread count with another" +license = "Apache-2.0" + +[targets.box2d-determinism] +kind = "bin" +main = "src/main.cpp" + +[build] +# The falling-hinges setup does float arithmetic of its own: compiled as +# upstream compiles its tests, without FMA contraction. +cxxflags = ["-ffp-contract=off"] + +[dependencies] +"mcpplibs.mwrap.box2d" = { path = "../.." } diff --git a/examples/box2d/src/main.cpp b/examples/box2d/src/main.cpp new file mode 100644 index 0000000..6151789 --- /dev/null +++ b/examples/box2d/src/main.cpp @@ -0,0 +1,94 @@ +// box2d-determinism — does this machine simulate Box2D bit for bit as every +// other one does? Runs Box2D's determinism scenes through import +// mcpplibs.mwrap.box2d and prints, per run, the step on which the world fell +// asleep and three hashes of its state: the djb2 hash of every body's final +// transform, a hash of the per-step event counts, and b2World_GetStateHash. +// Two machines, compilers, worker counts or SIMD paths agree when the lines +// agree. +// +// box2d-determinism [--scene hinges|pyramid|all] [--workers N] [--sse2] +// [--tasks] [--steps N] [--json] +// +// --scene hinges: upstream's falling hinges (pinned upstream: sleep +// step 299, transforms 0x465F3DF4); pyramid: a struck 20-row +// pyramid, sleep off, --steps steps (600); all (the default) +// --workers worker count (1: serial; more: Box2D's built-in scheduler) +// --sse2 take the 4-wide SSE2 solver where the CPU would take AVX2 +// --tasks run tasks through a user task system (serially, on the caller) +// --json one JSON object per run instead of a line of text +import std; +import mcpplibs.mwrap.box2d; + +#include "scenes.hpp" + +namespace { + +int usage() { + std::cerr << "usage: box2d-determinism [--scene hinges|pyramid|all] [--workers N] [--sse2] [--tasks] [--steps N] [--json]\n"; + return 2; +} + +void print(std::string_view scene, const box2d_scenes::Config& c, const box2d_scenes::Result& r, bool json) { + if (json) { + std::printf("{\"scene\":\"%.*s\",\"workers\":%d,\"sse2Fallback\":%s,\"tasks\":\"%s\",\"avx2\":%s," + "\"steps\":%d,\"sleepStep\":%d,\"transforms\":\"0x%08X\",\"events\":\"0x%08X\"," + "\"state\":\"0x%016llX\"}\n", + static_cast(scene.size()), scene.data(), c.workers, c.sse2Fallback ? "true" : "false", + c.tasks ? "user" : "built-in", b2IsAVX2Available() ? "true" : "false", r.steps, r.sleepStep, + r.transforms, r.events, static_cast(r.state)); + } else { + std::printf("%-14.*s workers=%d sse2Fallback=%d tasks=%-8s steps=%d sleepStep=%d transforms=0x%08X " + "events=0x%08X state=0x%016llX\n", + static_cast(scene.size()), scene.data(), c.workers, c.sse2Fallback ? 1 : 0, + c.tasks ? "user" : "built-in", r.steps, r.sleepStep, r.transforms, r.events, + static_cast(r.state)); + } +} + +} // namespace + +int main(int argc, char** argv) { + std::string scene = "all"; + box2d_scenes::Config config; + int steps = 600; + bool json = false; + for (int i = 1; i < argc; ++i) { + const std::string_view arg = argv[i]; + auto value = [&]() -> std::optional { + if (i + 1 >= argc) return std::nullopt; + return std::string_view(argv[++i]); + }; + auto number = [&](int& out) { + auto v = value(); + if (!v) return false; + auto [end, error] = std::from_chars(v->data(), v->data() + v->size(), out); + return error == std::errc{} && end == v->data() + v->size(); + }; + if (arg == "--scene") { + auto v = value(); + if (!v || (*v != "hinges" && *v != "pyramid" && *v != "all")) return usage(); + scene = *v; + } else if (arg == "--workers") { + if (!number(config.workers) || config.workers < 1) return usage(); + } else if (arg == "--steps") { + if (!number(steps) || steps < 1) return usage(); + } else if (arg == "--sse2") { + config.sse2Fallback = true; + } else if (arg == "--tasks") { + config.tasks = true; + } else if (arg == "--json") { + json = true; + } else { + return usage(); + } + } + if (!json) { + b2Version v = b2GetVersion(); + std::printf("Box2D %d.%d.%d, %s, AVX2 %s\n", v.major, v.minor, v.revision, + b2IsDoublePrecision() ? "double precision" : "single precision", + b2IsAVX2Available() ? "available" : "not available"); + } + if (scene != "pyramid") print("falling-hinges", config, box2d_scenes::fallingHinges(config), json); + if (scene != "hinges") print("pyramid", config, box2d_scenes::pyramid(config, steps), json); + return 0; +} diff --git a/examples/box2d/src/scenes.hpp b/examples/box2d/src/scenes.hpp new file mode 100644 index 0000000..02747cb --- /dev/null +++ b/examples/box2d/src/scenes.hpp @@ -0,0 +1,194 @@ +// The determinism scenes, written against `import mcpplibs.mwrap.box2d;` (the +// importer imports the module before including this file): +// +// falling hinges upstream's own cross-platform determinism scene +// (shared/determinism.c, test/test_determinism.c): 80 +// boxes in 4 columns, hinged in pairs with limited, +// sprung, motorised revolute joints, rotated by ±0.1 rad +// through b2MakeRot (Box2D's own cosine and sine), fall +// and settle; stepped until every body sleeps. Upstream +// pins the step and the hash: 299, 0x465F3DF4. +// pyramid a 20-row pyramid of 210 unit boxes on a static ground, +// struck from the left by a heavy ball (radius 1, density +// 8, launched at 24 m/s), with sleep off, stepped exactly +// 600 times (10 s at 60 Hz, 4 sub-steps): the collapse is +// chaotic, so a difference in any bit of any step grows +// into the final transforms. Every initial coordinate and +// velocity is exact in binary (x = j + i/2 - 9.5, y = i + +// 1/2), so the scene's own arithmetic is the same with or +// without FMA contraction in the caller: what the hash +// measures is the library. +// +// Each run records the step on which the world fell asleep, the djb2 hash +// (b2Hash) of every body's final b2Transform bytes in creation order, a hash +// of the per-step event counts (contact begins, body moves, awake bodies), and +// b2World_GetStateHash, Box2D's deep hash of the world. +#pragma once + +namespace box2d_scenes { + +struct Config { + int workers = 1; // 1: serial; more: the built-in scheduler, or `tasks` + bool sse2Fallback = false; // the 4-wide solver where the CPU would take AVX2 + bool tasks = false; // a user task system (serial, on the caller) +}; + +struct Result { + int steps = 0; + int sleepStep = -1; + std::uint32_t transforms = 0; + std::uint32_t events = 0; + std::uint64_t state = 0; +}; + +inline void* runInline(b2TaskCallbackFcn* task, void* taskContext, void*) { + task(taskContext); + return nullptr; // executed serially: nothing to finish +} +inline void finishNothing(void*, void*) {} + +inline b2WorldId makeWorld(const Config& config, bool sleep = true) { + b2WorldDef def = b2DefaultWorldDef(); + def.enableSleep = sleep; + def.workerCount = config.workers; + if (config.tasks) { + def.enqueueTask = runInline; + def.finishTask = finishNothing; + } + b2WorldId world = b2CreateWorld(&def); + b2World_EnableSSE2Fallback(world, config.sse2Fallback); + return world; +} + +inline std::uint32_t hashTransforms(const std::vector& bodies) { + std::uint32_t hash = mcpplibs::mwrap::box2d::macros::B2_HASH_INIT; + for (b2BodyId body : bodies) { + b2WorldTransform t = b2Body_GetTransform(body); + hash = b2Hash(hash, reinterpret_cast(&t), static_cast(sizeof t)); + } + return hash; +} + +// Steps `world` until `done` (or `limit` steps), recording the result. +template +Result step(b2WorldId world, const std::vector& bodies, int limit, Done done) { + Result r; + r.events = mcpplibs::mwrap::box2d::macros::B2_HASH_INIT; + while (r.steps < limit) { + b2World_Step(world, 1.0f / 60.0f, 4); + const int counts[3] = {b2World_GetContactEvents(world).beginCount, b2World_GetBodyEvents(world).moveCount, + b2World_GetAwakeBodyCount(world)}; + r.events = b2Hash(r.events, reinterpret_cast(counts), static_cast(sizeof counts)); + if (r.sleepStep < 0 && counts[1] == 0) r.sleepStep = r.steps; + ++r.steps; + if (done(r)) break; + } + r.transforms = hashTransforms(bodies); + r.state = b2World_GetStateHash(world); + return r; +} + +inline Result fallingHinges(const Config& config) { + b2WorldId world = makeWorld(config); + { + b2BodyDef bodyDef = b2DefaultBodyDef(); + bodyDef.position = b2Pos{0.0f, -1.0f}; + b2BodyId ground = b2CreateBody(world, &bodyDef); + b2Polygon box = b2MakeBox(40.0f, 1.0f); + b2ShapeDef shapeDef = b2DefaultShapeDef(); + b2CreatePolygonShape(ground, &shapeDef, &box); + } + const int columnCount = 4, rowCount = 20; + const float h = 0.25f; + b2Polygon box = b2MakeSquare(h); + b2ShapeDef shapeDef = b2DefaultShapeDef(); + b2RevoluteJointDef jointDef = b2DefaultRevoluteJointDef(); + jointDef.enableLimit = true; + jointDef.lowerAngle = -0.1f * mcpplibs::mwrap::box2d::macros::B2_PI; + jointDef.upperAngle = 0.2f * mcpplibs::mwrap::box2d::macros::B2_PI; + jointDef.enableSpring = true; + jointDef.hertz = 1.0f; + jointDef.dampingRatio = 1.0f; + jointDef.enableMotor = true; + jointDef.maxMotorTorque = 0.25f; + jointDef.base.localFrameA.p = b2Vec2{-h, h}; + jointDef.base.localFrameB.p = b2Vec2{-h, -h}; + jointDef.base.constraintHertz = 60.0f; + jointDef.base.constraintDampingRatio = 0.0f; + jointDef.base.drawScale = 0.5f; + + std::vector bodies; + const float offset = 0.4f * h, dx = 10.0f * h; + const float xBase = -0.5f * dx * (columnCount - 1.0f); + for (int j = 0; j < columnCount; ++j) { + const float x = xBase + j * dx; + b2BodyId prev{}; + for (int i = 0; i < rowCount; ++i) { + b2BodyDef bodyDef = b2DefaultBodyDef(); + bodyDef.type = b2_dynamicBody; + bodyDef.position.x = x + offset * i; + bodyDef.position.y = h + 2.0f * h * i; + bodyDef.rotation = b2MakeRot((i & 1) == 0 ? -0.1f : 0.1f); + b2BodyId body = b2CreateBody(world, &bodyDef); + if ((i & 1) == 0) { + prev = body; + } else { + jointDef.base.bodyIdA = prev; + jointDef.base.bodyIdB = body; + b2CreateRevoluteJoint(world, &jointDef); + prev = b2BodyId{}; + } + b2CreatePolygonShape(body, &shapeDef, &box); + bodies.push_back(body); + } + } + // Upstream hashes the transforms on the first step with no body moving. + Result r = step(world, bodies, 1000, [](const Result& r) { return r.sleepStep >= 0; }); + b2DestroyWorld(world); + return r; +} + +inline Result pyramid(const Config& config, int steps = 600) { + b2WorldId world = makeWorld(config, false); + { + b2BodyDef bodyDef = b2DefaultBodyDef(); + bodyDef.position = b2Pos{0.0f, -1.0f}; + b2BodyId ground = b2CreateBody(world, &bodyDef); + b2Polygon box = b2MakeBox(40.0f, 1.0f); + b2ShapeDef shapeDef = b2DefaultShapeDef(); + b2CreatePolygonShape(ground, &shapeDef, &box); + } + const int rows = 20; + b2Polygon box = b2MakeBox(0.5f, 0.5f); + b2ShapeDef shapeDef = b2DefaultShapeDef(); + std::vector bodies; + for (int i = 0; i < rows; ++i) { + for (int j = 0; j < rows - i; ++j) { + b2BodyDef bodyDef = b2DefaultBodyDef(); + bodyDef.type = b2_dynamicBody; + bodyDef.position = b2Pos{static_cast(j) + 0.5f * static_cast(i) - 9.5f, + static_cast(i) + 0.5f}; + b2BodyId body = b2CreateBody(world, &bodyDef); + b2CreatePolygonShape(body, &shapeDef, &box); + bodies.push_back(body); + } + } + { + b2BodyDef bodyDef = b2DefaultBodyDef(); + bodyDef.type = b2_dynamicBody; + bodyDef.position = b2Pos{-30.0f, 3.0f}; + bodyDef.linearVelocity = b2Vec2{24.0f, 4.0f}; + bodyDef.isBullet = true; + b2BodyId ball = b2CreateBody(world, &bodyDef); + b2Circle circle = {{0.0f, 0.0f}, 1.0f}; + b2ShapeDef ballDef = b2DefaultShapeDef(); + ballDef.density = 8.0f; + b2CreateCircleShape(ball, &ballDef, &circle); + bodies.push_back(ball); + } + Result r = step(world, bodies, steps, [](const Result&) { return false; }); + b2DestroyWorld(world); + return r; +} + +} // namespace box2d_scenes diff --git a/include/mwrap/box2d/macros.hpp b/include/mwrap/box2d/macros.hpp new file mode 100644 index 0000000..c932bf2 --- /dev/null +++ b/include/mwrap/box2d/macros.hpp @@ -0,0 +1,17 @@ +// — Box2D's macros as macros, for the ones a C++ +// entity cannot be: the declaration specifiers (BOX2D_EXPORT, B2_API, +// B2_INLINE, B2_FORCE_INLINE, B2_ID_INLINE, B2_ALIGN_AS), the initializer +// syntax (B2_LITERAL, B2_ZERO_INIT, B2_NULL_ID) and the assertions +// (B2_ASSERT, B2_VALIDATE, B2_BREAKPOINT). +// +// Include it before `import mcpplibs.mwrap.box2d;`: it is , as +// the module sees it (compat.box2d is a static library in single precision, +// so BOX2D_EXPORT is empty and b2Pos is b2Vec2). Every other macro is defined +// too, and while a macro is defined it expands even inside +// `mcpplibs::mwrap::box2d::macros::NAME`; a function-like one does not when its +// name is followed by `)`: `(macros::B2_ID_EQUALS)(a, b)` is the function. The +// header's `static const` objects (b2_nullBodyId …) are its own here, and +// `mcpplibs::mwrap::box2d::b2_nullBodyId` the module's constant of the same +// value. +#pragma once +#include diff --git a/mcpp.toml b/mcpp.toml new file mode 100644 index 0000000..3269c4d --- /dev/null +++ b/mcpp.toml @@ -0,0 +1,48 @@ +# Module wrapper; upstream API preserved. Policy: mcpp-index/docs/mwrap.md. +[package] +namespace = "mcpplibs.mwrap" +name = "box2d" +version = "3.2.0" +description = "Box2D 3.2.0 C++ modules for mcpp (import mcpplibs.mwrap.box2d;): the 2D rigid-body physics engine of box2d.h over compat.box2d, its constants and id macros as constexpr, cross-platform deterministic" +license = "Apache-2.0" +authors = ["mcpp-community"] +repo = "https://github.com/mcpplibs/box2d-m" +platforms = ["linux", "macos", "windows"] + +[targets.box2d] +kind = "lib" + +[build] +sources = ["src/*.cppm"] +# : the macros a C++ entity cannot be. +include_dirs = ["include"] +# Box2D's deterministic math (upstream CMakeLists.txt): no contraction of +# a*b+c into an FMA. The module's inline math (b2MakeRot, b2MulAdd, +# b2TransformPoint …) is compiled here and in this repository's tests with it, +# as upstream compiles its library and its tests. +cxxflags = ["-ffp-contract=off"] + +[dependencies] +compat.box2d = "3.2.0" + +[dev-dependencies] +compat.gtest = "1.15.2" + +[package.metadata.mwrap] +upstream-version = "3.2.0" + +[[runtime.deploy]] +from = "LICENSE" +to = "licenses/box2d" + +[[runtime.deploy]] +from = "NOTICE" +to = "licenses/box2d" + +[[runtime.deploy]] +from = "upstream/box2d/README.md" +to = "licenses/box2d/upstream/box2d" + +[[runtime.deploy]] +from = "upstream/box2d/LICENSE" +to = "licenses/box2d/upstream/box2d" diff --git a/src/box2d.cppm b/src/box2d.cppm new file mode 100644 index 0000000..46e61eb --- /dev/null +++ b/src/box2d.cppm @@ -0,0 +1,959 @@ +// Generated by tools/gen_exports.py from the Box2D 3.2.0 headers — do not edit +// by hand. +// +// mcpplibs.mwrap.box2d — Box2D v3, the 2D rigid-body physics engine for games, +// as a C++ module. Every exported name is the upstream global of +// , unchanged: the types, enums and their enumerators, every +// B2_API function (worlds, bodies, shapes, chains, joints, contacts, events, +// queries, the collision and geometry functions, recording and replay), the +// inline math and id functions, the C++ vector operators and +// b2_lengthUnitsPerMeter. The headers' `static const` objects (internal +// linkage) are constants of the same type and value in mcpplibs::mwrap::box2d. +// The root also re-exports the macro mappings. The library is compat.box2d, +// built from the same release (single precision, upstream's default SIMD, +// -ffp-contract=off). +module; +#include + +export module mcpplibs.mwrap.box2d; + +export import mcpplibs.mwrap.box2d.macros; + +// types: structs, enums, function types and aliases (110) +export using ::b2AABB; +export using ::b2AllocCallbackFcn; +export using ::b2AssertCallbackFcn; +export using ::b2BodyDef; +export using ::b2BodyEvents; +export using ::b2BodyId; +export using ::b2BodyMoveEvent; +export using ::b2BodyType; +export using ::b2BoxCastInput; +export using ::b2Capacity; +export using ::b2Capsule; +export using ::b2CastOutput; +export using ::b2CastResultCallbackFcn; +export using ::b2ChainDef; +export using ::b2ChainId; +export using ::b2ChainSegment; +export using ::b2Circle; +export using ::b2CollisionPlane; +export using ::b2ContactBeginTouchEvent; +export using ::b2ContactData; +export using ::b2ContactEndTouchEvent; +export using ::b2ContactEvents; +export using ::b2ContactHitEvent; +export using ::b2ContactId; +export using ::b2CosSin; +export using ::b2Counters; +export using ::b2CustomFilterCallbackFcn; +export using ::b2DebugDraw; +export using ::b2DistanceInput; +export using ::b2DistanceJointDef; +export using ::b2DistanceOutput; +export using ::b2EnqueueTaskCallbackFcn; +export using ::b2ExplosionDef; +export using ::b2Filter; +export using ::b2FilterJointDef; +export using ::b2FinishTaskCallbackFcn; +export using ::b2FreeCallbackFcn; +export using ::b2FrictionCallbackFcn; +export using ::b2HexColor; +export using ::b2Hull; +export using ::b2JointDef; +export using ::b2JointEvent; +export using ::b2JointEvents; +export using ::b2JointId; +export using ::b2JointType; +export using ::b2LocalManifold; +export using ::b2LocalManifoldPoint; +export using ::b2LogCallbackFcn; +export using ::b2Manifold; +export using ::b2ManifoldPoint; +export using ::b2MassData; +export using ::b2Mat22; +export using ::b2MotionLocks; +export using ::b2MotorJointDef; +export using ::b2MoverJointDef; +export using ::b2OverlapResultCallbackFcn; +export using ::b2Plane; +export using ::b2PlaneResult; +export using ::b2PlaneResultCallbackFcn; +export using ::b2PlaneSolverResult; +export using ::b2PogoJointDef; +export using ::b2Polygon; +export using ::b2Pos; +export using ::b2PreContinuousCallbackFcn; +export using ::b2PreSolveCallbackFcn; +export using ::b2PrismaticJointDef; +export using ::b2Profile; +export using ::b2QueryFilter; +export using ::b2RayCastInput; +export using ::b2RayResult; +export using ::b2ReplayInfo; +export using ::b2ReplayQueryHit; +export using ::b2ReplayQueryInfo; +export using ::b2ReplayQueryType; +export using ::b2RestitutionCallbackFcn; +export using ::b2RevoluteJointDef; +export using ::b2Rot; +export using ::b2Segment; +export using ::b2SegmentDistanceResult; +export using ::b2SensorBeginTouchEvent; +export using ::b2SensorEndTouchEvent; +export using ::b2SensorEvents; +export using ::b2ShapeCastInput; +export using ::b2ShapeCastPairInput; +export using ::b2ShapeDef; +export using ::b2ShapeId; +export using ::b2ShapeProxy; +export using ::b2ShapeType; +export using ::b2Simplex; +export using ::b2SimplexCache; +export using ::b2SimplexVertex; +export using ::b2SurfaceMaterial; +export using ::b2Sweep; +export using ::b2TOIInput; +export using ::b2TOIOutput; +export using ::b2TOIState; +export using ::b2TaskCallbackFcn; +export using ::b2Transform; +export using ::b2TreeBoxCastCallbackFcn; +export using ::b2TreeQueryCallbackFcn; +export using ::b2TreeRayCastCallbackFcn; +export using ::b2TreeStats; +export using ::b2Vec2; +export using ::b2Version; +export using ::b2WeldJointDef; +export using ::b2WheelJointDef; +export using ::b2WorldCastOutput; +export using ::b2WorldDef; +export using ::b2WorldId; +export using ::b2WorldTransform; + +// opaque structs, declared and not defined (3) +export using ::b2DynamicTree; +export using ::b2Recording; +export using ::b2Replay; + +// enumerators (176) +// b2TOIState (5) +export using ::b2_toiStateUnknown; +export using ::b2_toiStateFailed; +export using ::b2_toiStateOverlapped; +export using ::b2_toiStateHit; +export using ::b2_toiStateSeparated; +// b2BodyType (4) +export using ::b2_staticBody; +export using ::b2_kinematicBody; +export using ::b2_dynamicBody; +export using ::b2_bodyTypeCount; +// b2ShapeType (6) +export using ::b2_circleShape; +export using ::b2_capsuleShape; +export using ::b2_segmentShape; +export using ::b2_polygonShape; +export using ::b2_chainSegmentShape; +export using ::b2_shapeTypeCount; +// b2JointType (9) +export using ::b2_distanceJoint; +export using ::b2_filterJoint; +export using ::b2_motorJoint; +export using ::b2_moverJoint; +export using ::b2_pogoJoint; +export using ::b2_prismaticJoint; +export using ::b2_revoluteJoint; +export using ::b2_weldJoint; +export using ::b2_wheelJoint; +// b2HexColor (145) +export using ::b2_colorAliceBlue; +export using ::b2_colorAntiqueWhite; +export using ::b2_colorAqua; +export using ::b2_colorAquamarine; +export using ::b2_colorAzure; +export using ::b2_colorBeige; +export using ::b2_colorBisque; +export using ::b2_colorBlack; +export using ::b2_colorBlanchedAlmond; +export using ::b2_colorBlue; +export using ::b2_colorBlueViolet; +export using ::b2_colorBrown; +export using ::b2_colorBurlywood; +export using ::b2_colorCadetBlue; +export using ::b2_colorChartreuse; +export using ::b2_colorChocolate; +export using ::b2_colorCoral; +export using ::b2_colorCornflowerBlue; +export using ::b2_colorCornsilk; +export using ::b2_colorCrimson; +export using ::b2_colorCyan; +export using ::b2_colorDarkBlue; +export using ::b2_colorDarkCyan; +export using ::b2_colorDarkGoldenRod; +export using ::b2_colorDarkGray; +export using ::b2_colorDarkGreen; +export using ::b2_colorDarkKhaki; +export using ::b2_colorDarkMagenta; +export using ::b2_colorDarkOliveGreen; +export using ::b2_colorDarkOrange; +export using ::b2_colorDarkOrchid; +export using ::b2_colorDarkRed; +export using ::b2_colorDarkSalmon; +export using ::b2_colorDarkSeaGreen; +export using ::b2_colorDarkSlateBlue; +export using ::b2_colorDarkSlateGray; +export using ::b2_colorDarkTurquoise; +export using ::b2_colorDarkViolet; +export using ::b2_colorDeepPink; +export using ::b2_colorDeepSkyBlue; +export using ::b2_colorDimGray; +export using ::b2_colorDodgerBlue; +export using ::b2_colorFireBrick; +export using ::b2_colorFloralWhite; +export using ::b2_colorForestGreen; +export using ::b2_colorFuchsia; +export using ::b2_colorGainsboro; +export using ::b2_colorGhostWhite; +export using ::b2_colorGold; +export using ::b2_colorGoldenRod; +export using ::b2_colorGray; +export using ::b2_colorGreen; +export using ::b2_colorGreenYellow; +export using ::b2_colorHoneyDew; +export using ::b2_colorHotPink; +export using ::b2_colorIndianRed; +export using ::b2_colorIndigo; +export using ::b2_colorIvory; +export using ::b2_colorKhaki; +export using ::b2_colorLavender; +export using ::b2_colorLavenderBlush; +export using ::b2_colorLawnGreen; +export using ::b2_colorLemonChiffon; +export using ::b2_colorLightBlue; +export using ::b2_colorLightCoral; +export using ::b2_colorLightCyan; +export using ::b2_colorLightGoldenRodYellow; +export using ::b2_colorLightGray; +export using ::b2_colorLightGreen; +export using ::b2_colorLightPink; +export using ::b2_colorLightSalmon; +export using ::b2_colorLightSeaGreen; +export using ::b2_colorLightSkyBlue; +export using ::b2_colorLightSlateGray; +export using ::b2_colorLightSteelBlue; +export using ::b2_colorLightYellow; +export using ::b2_colorLime; +export using ::b2_colorLimeGreen; +export using ::b2_colorLinen; +export using ::b2_colorMagenta; +export using ::b2_colorMaroon; +export using ::b2_colorMediumAquaMarine; +export using ::b2_colorMediumBlue; +export using ::b2_colorMediumOrchid; +export using ::b2_colorMediumPurple; +export using ::b2_colorMediumSeaGreen; +export using ::b2_colorMediumSlateBlue; +export using ::b2_colorMediumSpringGreen; +export using ::b2_colorMediumTurquoise; +export using ::b2_colorMediumVioletRed; +export using ::b2_colorMidnightBlue; +export using ::b2_colorMintCream; +export using ::b2_colorMistyRose; +export using ::b2_colorMoccasin; +export using ::b2_colorNavajoWhite; +export using ::b2_colorNavy; +export using ::b2_colorOldLace; +export using ::b2_colorOlive; +export using ::b2_colorOliveDrab; +export using ::b2_colorOrange; +export using ::b2_colorOrangeRed; +export using ::b2_colorOrchid; +export using ::b2_colorPaleGoldenRod; +export using ::b2_colorPaleGreen; +export using ::b2_colorPaleTurquoise; +export using ::b2_colorPaleVioletRed; +export using ::b2_colorPapayaWhip; +export using ::b2_colorPeachPuff; +export using ::b2_colorPeru; +export using ::b2_colorPink; +export using ::b2_colorPlum; +export using ::b2_colorPowderBlue; +export using ::b2_colorPurple; +export using ::b2_colorRebeccaPurple; +export using ::b2_colorRed; +export using ::b2_colorRosyBrown; +export using ::b2_colorRoyalBlue; +export using ::b2_colorSaddleBrown; +export using ::b2_colorSalmon; +export using ::b2_colorSandyBrown; +export using ::b2_colorSeaGreen; +export using ::b2_colorSeaShell; +export using ::b2_colorSienna; +export using ::b2_colorSilver; +export using ::b2_colorSkyBlue; +export using ::b2_colorSlateBlue; +export using ::b2_colorSlateGray; +export using ::b2_colorSnow; +export using ::b2_colorSpringGreen; +export using ::b2_colorSteelBlue; +export using ::b2_colorTan; +export using ::b2_colorTeal; +export using ::b2_colorThistle; +export using ::b2_colorTomato; +export using ::b2_colorTurquoise; +export using ::b2_colorViolet; +export using ::b2_colorWheat; +export using ::b2_colorWhite; +export using ::b2_colorWhiteSmoke; +export using ::b2_colorYellow; +export using ::b2_colorYellowGreen; +export using ::b2_colorBox2DRed; +export using ::b2_colorBox2DBlue; +export using ::b2_colorBox2DGreen; +export using ::b2_colorBox2DYellow; +// b2ReplayQueryType (7) +export using ::b2_replayQueryOverlapAABB; +export using ::b2_replayQueryOverlapShape; +export using ::b2_replayQueryCastRay; +export using ::b2_replayQueryCastShape; +export using ::b2_replayQueryCollideMover; +export using ::b2_replayQueryCastRayClosest; +export using ::b2_replayQueryCastMover; + +// B2_API functions (506) +export using ::b2Atan2; +export using ::b2Body_ApplyAngularImpulse; +export using ::b2Body_ApplyForce; +export using ::b2Body_ApplyForceToCenter; +export using ::b2Body_ApplyLinearImpulse; +export using ::b2Body_ApplyLinearImpulseToCenter; +export using ::b2Body_ApplyTorque; +export using ::b2Body_ClearForces; +export using ::b2Body_ComputeBounds; +export using ::b2Body_Disable; +export using ::b2Body_Enable; +export using ::b2Body_EnableContactEvents; +export using ::b2Body_EnableContactRecycling; +export using ::b2Body_EnableHitEvents; +export using ::b2Body_EnableSleeping; +export using ::b2Body_GetAngularDamping; +export using ::b2Body_GetAngularVelocity; +export using ::b2Body_GetContactCapacity; +export using ::b2Body_GetContactData; +export using ::b2Body_GetGravityScale; +export using ::b2Body_GetJointCount; +export using ::b2Body_GetJoints; +export using ::b2Body_GetLinearDamping; +export using ::b2Body_GetLinearVelocity; +export using ::b2Body_GetLocalCenter; +export using ::b2Body_GetLocalPoint; +export using ::b2Body_GetLocalPointVelocity; +export using ::b2Body_GetLocalVector; +export using ::b2Body_GetMass; +export using ::b2Body_GetMassData; +export using ::b2Body_GetMaxExtent; +export using ::b2Body_GetMaxExtentFromOrigin; +export using ::b2Body_GetMinExtent; +export using ::b2Body_GetMotionLocks; +export using ::b2Body_GetName; +export using ::b2Body_GetPosition; +export using ::b2Body_GetRotation; +export using ::b2Body_GetRotationalInertia; +export using ::b2Body_GetSafetyFactor; +export using ::b2Body_GetShapeCount; +export using ::b2Body_GetShapes; +export using ::b2Body_GetSleepThreshold; +export using ::b2Body_GetTransform; +export using ::b2Body_GetType; +export using ::b2Body_GetUserData; +export using ::b2Body_GetWorld; +export using ::b2Body_GetWorldCenter; +export using ::b2Body_GetWorldPoint; +export using ::b2Body_GetWorldPointVelocity; +export using ::b2Body_GetWorldVector; +export using ::b2Body_IsAwake; +export using ::b2Body_IsBullet; +export using ::b2Body_IsContactRecyclingEnabled; +export using ::b2Body_IsEnabled; +export using ::b2Body_IsSleepingEnabled; +export using ::b2Body_IsValid; +export using ::b2Body_SetAngularDamping; +export using ::b2Body_SetAngularVelocity; +export using ::b2Body_SetAwake; +export using ::b2Body_SetBullet; +export using ::b2Body_SetGravityScale; +export using ::b2Body_SetLinearDamping; +export using ::b2Body_SetLinearVelocity; +export using ::b2Body_SetMassData; +export using ::b2Body_SetMotionLocks; +export using ::b2Body_SetName; +export using ::b2Body_SetSafetyFactor; +export using ::b2Body_SetSleepThreshold; +export using ::b2Body_SetTargetTransform; +export using ::b2Body_SetTransform; +export using ::b2Body_SetType; +export using ::b2Body_SetUserData; +export using ::b2Body_UpdateMassFromShapes; +export using ::b2Body_WakeTouching; +export using ::b2CastRayCapsule; +export using ::b2CastRayCircle; +export using ::b2CastRayPolygon; +export using ::b2CastRaySegment; +export using ::b2CastShape; +export using ::b2CastShapeCapsule; +export using ::b2CastShapeCircle; +export using ::b2CastShapePolygon; +export using ::b2CastShapeSegment; +export using ::b2Chain_GetSegmentCount; +export using ::b2Chain_GetSegments; +export using ::b2Chain_GetSurfaceMaterial; +export using ::b2Chain_GetWorld; +export using ::b2Chain_IsValid; +export using ::b2Chain_SetAllSurfaceMaterials; +export using ::b2Chain_SetSurfaceMaterial; +export using ::b2ClipVector; +export using ::b2CollideCapsuleAndCircle; +export using ::b2CollideCapsules; +export using ::b2CollideChainSegmentAndCapsule; +export using ::b2CollideChainSegmentAndCircle; +export using ::b2CollideChainSegmentAndPolygon; +export using ::b2CollideCircles; +export using ::b2CollidePolygonAndCapsule; +export using ::b2CollidePolygonAndCircle; +export using ::b2CollidePolygons; +export using ::b2CollideSegmentAndCapsule; +export using ::b2CollideSegmentAndCircle; +export using ::b2CollideSegmentAndPolygon; +export using ::b2ComputeCapsuleBounds; +export using ::b2ComputeCapsuleMass; +export using ::b2ComputeCircleBounds; +export using ::b2ComputeCircleMass; +export using ::b2ComputeCosSin; +export using ::b2ComputeHull; +export using ::b2ComputePolygonBounds; +export using ::b2ComputePolygonMass; +export using ::b2ComputeRotationBetweenUnitVectors; +export using ::b2ComputeSegmentBounds; +export using ::b2ComputeSegmentDistance; +export using ::b2ComputeShapeDistance; +export using ::b2ComputeTimeOfImpact; +export using ::b2Contact_GetData; +export using ::b2Contact_IsValid; +export using ::b2CreateBody; +export using ::b2CreateCapsuleShape; +export using ::b2CreateChain; +export using ::b2CreateChainSegmentShape; +export using ::b2CreateCircleShape; +export using ::b2CreateDistanceJoint; +export using ::b2CreateDynamicTree; +export using ::b2CreateFilterJoint; +export using ::b2CreateMotorJoint; +export using ::b2CreateMoverJoint; +export using ::b2CreatePogoJoint; +export using ::b2CreatePolygonShape; +export using ::b2CreatePrismaticJoint; +export using ::b2CreateRecording; +export using ::b2CreateRecordingFromFile; +export using ::b2CreateReplay; +export using ::b2CreateRevoluteJoint; +export using ::b2CreateSegmentShape; +export using ::b2CreateTreeProxy; +export using ::b2CreateWeldJoint; +export using ::b2CreateWheelJoint; +export using ::b2CreateWorld; +export using ::b2CreateWorldFromSnapshot; +export using ::b2DefaultBodyDef; +export using ::b2DefaultChainDef; +export using ::b2DefaultDebugDraw; +export using ::b2DefaultDistanceJointDef; +export using ::b2DefaultExplosionDef; +export using ::b2DefaultFilter; +export using ::b2DefaultFilterJointDef; +export using ::b2DefaultMotorJointDef; +export using ::b2DefaultMoverJointDef; +export using ::b2DefaultPogoJointDef; +export using ::b2DefaultPrismaticJointDef; +export using ::b2DefaultQueryFilter; +export using ::b2DefaultRevoluteJointDef; +export using ::b2DefaultShapeDef; +export using ::b2DefaultSurfaceMaterial; +export using ::b2DefaultWeldJointDef; +export using ::b2DefaultWheelJointDef; +export using ::b2DefaultWorldDef; +export using ::b2DestroyBody; +export using ::b2DestroyChain; +export using ::b2DestroyDynamicTree; +export using ::b2DestroyJoint; +export using ::b2DestroyRecording; +export using ::b2DestroyReplay; +export using ::b2DestroyShape; +export using ::b2DestroyTreeProxy; +export using ::b2DestroyWorld; +export using ::b2DistanceJoint_EnableLimit; +export using ::b2DistanceJoint_EnableMotor; +export using ::b2DistanceJoint_EnableSpring; +export using ::b2DistanceJoint_GetCurrentLength; +export using ::b2DistanceJoint_GetCurrentMotorForce; +export using ::b2DistanceJoint_GetLowerLimit; +export using ::b2DistanceJoint_GetMaxMotorForce; +export using ::b2DistanceJoint_GetRestLength; +export using ::b2DistanceJoint_GetSpringDampingRatio; +export using ::b2DistanceJoint_GetSpringForce; +export using ::b2DistanceJoint_GetSpringForceRange; +export using ::b2DistanceJoint_GetSpringHertz; +export using ::b2DistanceJoint_GetTargetMotorSpeed; +export using ::b2DistanceJoint_GetUpperLimit; +export using ::b2DistanceJoint_IsLimitEnabled; +export using ::b2DistanceJoint_IsMotorEnabled; +export using ::b2DistanceJoint_IsSpringEnabled; +export using ::b2DistanceJoint_SetLimits; +export using ::b2DistanceJoint_SetMaxMotorForce; +export using ::b2DistanceJoint_SetRestLength; +export using ::b2DistanceJoint_SetSpringDampingRatio; +export using ::b2DistanceJoint_SetSpringForceRange; +export using ::b2DistanceJoint_SetSpringHertz; +export using ::b2DistanceJoint_SetTargetMotorSpeed; +export using ::b2DynamicTree_CastBox; +export using ::b2DynamicTree_CastRay; +export using ::b2DynamicTree_GetAreaRatio; +export using ::b2DynamicTree_GetByteCount; +export using ::b2DynamicTree_GetCategoryBits; +export using ::b2DynamicTree_GetHeight; +export using ::b2DynamicTree_GetProxyBounds; +export using ::b2DynamicTree_GetProxyCount; +export using ::b2DynamicTree_GetRootBounds; +export using ::b2DynamicTree_GetUserData; +export using ::b2DynamicTree_MoveProxy; +export using ::b2DynamicTree_OverlapAABB; +export using ::b2DynamicTree_OverlapAABBAll; +export using ::b2DynamicTree_SetCategoryBits; +export using ::b2DynamicTree_Validate; +export using ::b2GetByteCount; +export using ::b2GetGraphColor; +export using ::b2GetLengthUnitsPerMeter; +export using ::b2GetMilliseconds; +export using ::b2GetMillisecondsAndReset; +export using ::b2GetSweepTransform; +export using ::b2GetTicks; +export using ::b2GetVersion; +export using ::b2Hash; +export using ::b2IsAVX2Available; +export using ::b2IsDoublePrecision; +export using ::b2IsValidAABB; +export using ::b2IsValidFloat; +export using ::b2IsValidPlane; +export using ::b2IsValidPos; +export using ::b2IsValidRay; +export using ::b2IsValidRot; +export using ::b2IsValidTransform; +export using ::b2IsValidVec2; +export using ::b2IsValidWorldTransform; +export using ::b2Joint_GetAngularSeparation; +export using ::b2Joint_GetBodyA; +export using ::b2Joint_GetBodyB; +export using ::b2Joint_GetCollideConnected; +export using ::b2Joint_GetConstraintForce; +export using ::b2Joint_GetConstraintTorque; +export using ::b2Joint_GetConstraintTuning; +export using ::b2Joint_GetForceThreshold; +export using ::b2Joint_GetLinearSeparation; +export using ::b2Joint_GetLocalFrameA; +export using ::b2Joint_GetLocalFrameB; +export using ::b2Joint_GetTorqueThreshold; +export using ::b2Joint_GetType; +export using ::b2Joint_GetUserData; +export using ::b2Joint_GetWorld; +export using ::b2Joint_IsValid; +export using ::b2Joint_SetCollideConnected; +export using ::b2Joint_SetConstraintTuning; +export using ::b2Joint_SetForceThreshold; +export using ::b2Joint_SetLocalFrameA; +export using ::b2Joint_SetLocalFrameB; +export using ::b2Joint_SetTorqueThreshold; +export using ::b2Joint_SetUserData; +export using ::b2Joint_WakeBodies; +export using ::b2MakeBox; +export using ::b2MakeOffsetBox; +export using ::b2MakeOffsetPolygon; +export using ::b2MakeOffsetProxy; +export using ::b2MakeOffsetRoundedBox; +export using ::b2MakeOffsetRoundedPolygon; +export using ::b2MakePolygon; +export using ::b2MakeProxy; +export using ::b2MakeRoundedBox; +export using ::b2MakeSquare; +export using ::b2MotorJoint_GetAngularDampingRatio; +export using ::b2MotorJoint_GetAngularHertz; +export using ::b2MotorJoint_GetAngularVelocity; +export using ::b2MotorJoint_GetLinearDampingRatio; +export using ::b2MotorJoint_GetLinearHertz; +export using ::b2MotorJoint_GetLinearVelocity; +export using ::b2MotorJoint_GetMaxSpringForce; +export using ::b2MotorJoint_GetMaxSpringTorque; +export using ::b2MotorJoint_GetMaxVelocityForce; +export using ::b2MotorJoint_GetMaxVelocityTorque; +export using ::b2MotorJoint_SetAngularDampingRatio; +export using ::b2MotorJoint_SetAngularHertz; +export using ::b2MotorJoint_SetAngularVelocity; +export using ::b2MotorJoint_SetLinearDampingRatio; +export using ::b2MotorJoint_SetLinearHertz; +export using ::b2MotorJoint_SetLinearVelocity; +export using ::b2MotorJoint_SetMaxSpringForce; +export using ::b2MotorJoint_SetMaxSpringTorque; +export using ::b2MotorJoint_SetMaxVelocityForce; +export using ::b2MotorJoint_SetMaxVelocityTorque; +export using ::b2MoverJoint_GetLinearVelocity; +export using ::b2MoverJoint_GetMaxVelocityForce; +export using ::b2MoverJoint_SetLinearVelocity; +export using ::b2MoverJoint_SetMaxVelocityForce; +export using ::b2PogoJoint_GetCurrentLength; +export using ::b2PogoJoint_GetCurrentSpeed; +export using ::b2PogoJoint_GetImpulse; +export using ::b2PogoJoint_GetRestLength; +export using ::b2PogoJoint_GetSpringDampingRatio; +export using ::b2PogoJoint_GetSpringHertz; +export using ::b2PogoJoint_SetRestLength; +export using ::b2PogoJoint_SetSpringDampingRatio; +export using ::b2PogoJoint_SetSpringHertz; +export using ::b2PointInCapsule; +export using ::b2PointInCircle; +export using ::b2PointInPolygon; +export using ::b2PrismaticJoint_EnableLimit; +export using ::b2PrismaticJoint_EnableMotor; +export using ::b2PrismaticJoint_EnableSpring; +export using ::b2PrismaticJoint_GetCurrentMotorForce; +export using ::b2PrismaticJoint_GetCurrentSpeed; +export using ::b2PrismaticJoint_GetCurrentTranslation; +export using ::b2PrismaticJoint_GetLowerLimit; +export using ::b2PrismaticJoint_GetMaxMotorForce; +export using ::b2PrismaticJoint_GetSpringDampingRatio; +export using ::b2PrismaticJoint_GetSpringHertz; +export using ::b2PrismaticJoint_GetTargetMotorSpeed; +export using ::b2PrismaticJoint_GetTargetTranslation; +export using ::b2PrismaticJoint_GetUpperLimit; +export using ::b2PrismaticJoint_IsLimitEnabled; +export using ::b2PrismaticJoint_IsMotorEnabled; +export using ::b2PrismaticJoint_IsSpringEnabled; +export using ::b2PrismaticJoint_SetLimits; +export using ::b2PrismaticJoint_SetMaxMotorForce; +export using ::b2PrismaticJoint_SetSpringDampingRatio; +export using ::b2PrismaticJoint_SetSpringHertz; +export using ::b2PrismaticJoint_SetTargetMotorSpeed; +export using ::b2PrismaticJoint_SetTargetTranslation; +export using ::b2Recording_GetData; +export using ::b2Recording_GetSize; +export using ::b2Recording_SaveToFile; +export using ::b2Replay_DrawFrameQueries; +export using ::b2Replay_GetBodyCount; +export using ::b2Replay_GetBodyId; +export using ::b2Replay_GetDivergeFrame; +export using ::b2Replay_GetFrame; +export using ::b2Replay_GetFrameQuery; +export using ::b2Replay_GetFrameQueryCount; +export using ::b2Replay_GetFrameQueryHit; +export using ::b2Replay_GetInfo; +export using ::b2Replay_GetKeyframeBudget; +export using ::b2Replay_GetKeyframeBytes; +export using ::b2Replay_GetKeyframeInterval; +export using ::b2Replay_GetKeyframeMinInterval; +export using ::b2Replay_GetWorldId; +export using ::b2Replay_HasDiverged; +export using ::b2Replay_IsAtEnd; +export using ::b2Replay_Restart; +export using ::b2Replay_SeekFrame; +export using ::b2Replay_SetKeyframePolicy; +export using ::b2Replay_StepFrame; +export using ::b2RevoluteJoint_EnableLimit; +export using ::b2RevoluteJoint_EnableMotor; +export using ::b2RevoluteJoint_EnableSpring; +export using ::b2RevoluteJoint_GetCurrentAngle; +export using ::b2RevoluteJoint_GetCurrentMotorTorque; +export using ::b2RevoluteJoint_GetCurrentSpeed; +export using ::b2RevoluteJoint_GetLowerLimit; +export using ::b2RevoluteJoint_GetMaxMotorTorque; +export using ::b2RevoluteJoint_GetSpringDampingRatio; +export using ::b2RevoluteJoint_GetSpringHertz; +export using ::b2RevoluteJoint_GetTargetAngle; +export using ::b2RevoluteJoint_GetTargetMotorSpeed; +export using ::b2RevoluteJoint_GetUpperLimit; +export using ::b2RevoluteJoint_IsLimitEnabled; +export using ::b2RevoluteJoint_IsMotorEnabled; +export using ::b2RevoluteJoint_IsSpringEnabled; +export using ::b2RevoluteJoint_SetLimits; +export using ::b2RevoluteJoint_SetMaxMotorTorque; +export using ::b2RevoluteJoint_SetSpringDampingRatio; +export using ::b2RevoluteJoint_SetSpringHertz; +export using ::b2RevoluteJoint_SetTargetAngle; +export using ::b2RevoluteJoint_SetTargetMotorSpeed; +export using ::b2SetAllocator; +export using ::b2SetAssertCallback; +export using ::b2SetLengthUnitsPerMeter; +export using ::b2SetLogCallback; +export using ::b2Shape_ApplyWind; +export using ::b2Shape_AreContactEventsEnabled; +export using ::b2Shape_AreHitEventsEnabled; +export using ::b2Shape_ArePreSolveEventsEnabled; +export using ::b2Shape_AreSensorEventsEnabled; +export using ::b2Shape_CastRay; +export using ::b2Shape_ComputeMassData; +export using ::b2Shape_EnableContactEvents; +export using ::b2Shape_EnableHitEvents; +export using ::b2Shape_EnablePreSolveEvents; +export using ::b2Shape_EnableSensorEvents; +export using ::b2Shape_GetBody; +export using ::b2Shape_GetBounds; +export using ::b2Shape_GetCapsule; +export using ::b2Shape_GetChainSegment; +export using ::b2Shape_GetCircle; +export using ::b2Shape_GetClosestPoint; +export using ::b2Shape_GetContactCapacity; +export using ::b2Shape_GetContactData; +export using ::b2Shape_GetDensity; +export using ::b2Shape_GetFilter; +export using ::b2Shape_GetFriction; +export using ::b2Shape_GetParentChain; +export using ::b2Shape_GetPolygon; +export using ::b2Shape_GetRestitution; +export using ::b2Shape_GetSegment; +export using ::b2Shape_GetSensorCapacity; +export using ::b2Shape_GetSensorData; +export using ::b2Shape_GetSurfaceMaterial; +export using ::b2Shape_GetType; +export using ::b2Shape_GetUserData; +export using ::b2Shape_GetUserMaterial; +export using ::b2Shape_GetWorld; +export using ::b2Shape_IsSensor; +export using ::b2Shape_IsValid; +export using ::b2Shape_SetCapsule; +export using ::b2Shape_SetChainSegment; +export using ::b2Shape_SetCircle; +export using ::b2Shape_SetDensity; +export using ::b2Shape_SetFilter; +export using ::b2Shape_SetFriction; +export using ::b2Shape_SetPolygon; +export using ::b2Shape_SetRestitution; +export using ::b2Shape_SetSegment; +export using ::b2Shape_SetSurfaceMaterial; +export using ::b2Shape_SetUserData; +export using ::b2Shape_SetUserMaterial; +export using ::b2Shape_TestPoint; +export using ::b2SolvePlanes; +export using ::b2TransformPolygon; +export using ::b2ValidateHull; +export using ::b2ValidateRecording; +export using ::b2WeldJoint_GetAngularDampingRatio; +export using ::b2WeldJoint_GetAngularHertz; +export using ::b2WeldJoint_GetLinearDampingRatio; +export using ::b2WeldJoint_GetLinearHertz; +export using ::b2WeldJoint_SetAngularDampingRatio; +export using ::b2WeldJoint_SetAngularHertz; +export using ::b2WeldJoint_SetLinearDampingRatio; +export using ::b2WeldJoint_SetLinearHertz; +export using ::b2WheelJoint_EnableLimit; +export using ::b2WheelJoint_EnableMotor; +export using ::b2WheelJoint_EnableSpring; +export using ::b2WheelJoint_GetCurrentMotorTorque; +export using ::b2WheelJoint_GetLowerLimit; +export using ::b2WheelJoint_GetMaxMotorTorque; +export using ::b2WheelJoint_GetSpringDampingRatio; +export using ::b2WheelJoint_GetSpringHertz; +export using ::b2WheelJoint_GetTargetMotorSpeed; +export using ::b2WheelJoint_GetUpperLimit; +export using ::b2WheelJoint_IsLimitEnabled; +export using ::b2WheelJoint_IsMotorEnabled; +export using ::b2WheelJoint_IsSpringEnabled; +export using ::b2WheelJoint_SetLimits; +export using ::b2WheelJoint_SetMaxMotorTorque; +export using ::b2WheelJoint_SetSpringDampingRatio; +export using ::b2WheelJoint_SetSpringHertz; +export using ::b2WheelJoint_SetTargetMotorSpeed; +export using ::b2World_CastMover; +export using ::b2World_CastRay; +export using ::b2World_CastRayClosest; +export using ::b2World_CastShape; +export using ::b2World_CollideMover; +export using ::b2World_Draw; +export using ::b2World_DumpMemoryStats; +export using ::b2World_EnableContinuous; +export using ::b2World_EnableRestitutionPropagation; +export using ::b2World_EnableSSE2Fallback; +export using ::b2World_EnableSleeping; +export using ::b2World_EnableWarmStarting; +export using ::b2World_Explode; +export using ::b2World_GetAwakeBodyCount; +export using ::b2World_GetBodyEvents; +export using ::b2World_GetBounds; +export using ::b2World_GetContactEvents; +export using ::b2World_GetContactRecycleDistance; +export using ::b2World_GetContactTuning; +export using ::b2World_GetCounters; +export using ::b2World_GetGravity; +export using ::b2World_GetHitEventThreshold; +export using ::b2World_GetJointEvents; +export using ::b2World_GetMaxCapacity; +export using ::b2World_GetMaxLinearSpeed; +export using ::b2World_GetProfile; +export using ::b2World_GetRestitutionIterations; +export using ::b2World_GetRestitutionThreshold; +export using ::b2World_GetSensorEvents; +export using ::b2World_GetSnapshot; +export using ::b2World_GetStateHash; +export using ::b2World_GetUserData; +export using ::b2World_GetWorkerCount; +export using ::b2World_IsContinuousEnabled; +export using ::b2World_IsRestitutionPropagationEnabled; +export using ::b2World_IsSleepingEnabled; +export using ::b2World_IsValid; +export using ::b2World_IsWarmStartingEnabled; +export using ::b2World_OverlapAABB; +export using ::b2World_OverlapShape; +export using ::b2World_RebuildStaticTree; +export using ::b2World_RestoreFromSnapshot; +export using ::b2World_SetContactRecycleDistance; +export using ::b2World_SetContactTuning; +export using ::b2World_SetCustomFilterCallback; +export using ::b2World_SetFrictionCallback; +export using ::b2World_SetGravity; +export using ::b2World_SetHitEventThreshold; +export using ::b2World_SetMaxLinearSpeed; +export using ::b2World_SetPreSolveCallback; +export using ::b2World_SetRestitutionCallback; +export using ::b2World_SetRestitutionIterations; +export using ::b2World_SetRestitutionThreshold; +export using ::b2World_SetUserData; +export using ::b2World_SetWorkerCount; +export using ::b2World_StartRecording; +export using ::b2World_Step; +export using ::b2World_StopRecording; +export using ::b2Yield; +// declared, and defined by the library, only under !defined( NDEBUG ) || defined( B2_ENABLE_ASSERT ) +#if !defined( NDEBUG ) || defined( B2_ENABLE_ASSERT ) +export using ::b2InternalAssert; +#endif + +// inline functions: B2_INLINE, B2_FORCE_INLINE and B2_ID_INLINE are `inline` in C++ (91) +export using ::b2AABB_Center; +export using ::b2AABB_Contains; +export using ::b2AABB_Extents; +export using ::b2AABB_Overlaps; +export using ::b2AABB_Union; +export using ::b2Abs; +export using ::b2AbsFloat; +export using ::b2AbsInt; +export using ::b2Add; +export using ::b2CeilingInt; +export using ::b2Clamp; +export using ::b2ClampFloat; +export using ::b2ClampInt; +export using ::b2ComputeAngularVelocity; +export using ::b2Cross; +export using ::b2CrossSV; +export using ::b2CrossVS; +export using ::b2Distance; +export using ::b2DistanceSquared; +export using ::b2Dot; +export using ::b2GetInverse22; +export using ::b2GetLengthAndNormalize; +export using ::b2IntegrateRotation; +export using ::b2InvMulRot; +export using ::b2InvMulTransforms; +export using ::b2InvMulWorldTransforms; +export using ::b2InvRotateVector; +export using ::b2InvTransformPoint; +export using ::b2InvTransformWorldPoint; +export using ::b2InvertRot; +export using ::b2IsNormalized; +export using ::b2IsNormalizedRot; +export using ::b2LeftPerp; +export using ::b2Length; +export using ::b2LengthSquared; +export using ::b2Lerp; +export using ::b2LerpPos; +export using ::b2LoadBodyId; +export using ::b2LoadChainId; +export using ::b2LoadContactId; +export using ::b2LoadJointId; +export using ::b2LoadShapeId; +export using ::b2LoadWorldId; +export using ::b2MakeAABB; +export using ::b2MakeRot; +export using ::b2MakeRotFromUnitVector; +export using ::b2MakeWorldTransform; +export using ::b2Max; +export using ::b2MaxFloat; +export using ::b2MaxInt; +export using ::b2Min; +export using ::b2MinFloat; +export using ::b2MinInt; +export using ::b2Mul; +export using ::b2MulAdd; +export using ::b2MulMV; +export using ::b2MulRot; +export using ::b2MulSV; +export using ::b2MulSub; +export using ::b2MulTransforms; +export using ::b2MulWorldTransforms; +export using ::b2NLerp; +export using ::b2Neg; +export using ::b2Normalize; +export using ::b2NormalizeRot; +export using ::b2OffsetPos; +export using ::b2PlaneSeparation; +export using ::b2RelativeAngle; +export using ::b2RightPerp; +export using ::b2Rot_GetAngle; +export using ::b2Rot_GetXAxis; +export using ::b2Rot_GetYAxis; +export using ::b2RotateVector; +export using ::b2RoundDownFloat; +export using ::b2RoundUpFloat; +export using ::b2Solve22; +export using ::b2SpringDamper; +export using ::b2StoreBodyId; +export using ::b2StoreChainId; +export using ::b2StoreContactId; +export using ::b2StoreJointId; +export using ::b2StoreShapeId; +export using ::b2StoreWorldId; +export using ::b2Sub; +export using ::b2SubPos; +export using ::b2ToPos; +export using ::b2ToRelativeTransform; +export using ::b2ToVec2; +export using ::b2TransformPoint; +export using ::b2TransformWorldPoint; +export using ::b2UnwindAngle; + +// the C++ vector operators of math_functions.h (8) +export using ::operator!=; +export using ::operator*; +export using ::operator*=; +export using ::operator+; +export using ::operator+=; +export using ::operator-; +export using ::operator-=; +export using ::operator==; + +// variables (1) +export using ::b2_lengthUnitsPerMeter; + +// The headers' `static const` objects (13) have internal linkage: a +// named module cannot export them. Each is a constant of the same type and +// initializer here, under the same name. +export namespace mcpplibs::mwrap::box2d { + +inline constexpr ::b2SimplexCache b2_emptySimplexCache = B2_ZERO_INIT; +inline constexpr ::b2WorldId b2_nullWorldId = B2_NULL_ID; +inline constexpr ::b2BodyId b2_nullBodyId = B2_NULL_ID; +inline constexpr ::b2ShapeId b2_nullShapeId = B2_NULL_ID; +inline constexpr ::b2ChainId b2_nullChainId = B2_NULL_ID; +inline constexpr ::b2JointId b2_nullJointId = B2_NULL_ID; +inline constexpr ::b2ContactId b2_nullContactId = B2_NULL_ID; +inline constexpr ::b2Vec2 b2Vec2_zero = { 0.0f, 0.0f }; +inline constexpr ::b2Rot b2Rot_identity = { 1.0f, 0.0f }; +inline constexpr ::b2Transform b2Transform_identity = { { 0.0f, 0.0f }, { 1.0f, 0.0f } }; +inline constexpr ::b2Mat22 b2Mat22_zero = { { 0.0f, 0.0f }, { 0.0f, 0.0f } }; +inline constexpr ::b2Pos b2Pos_zero = { 0.0f, 0.0f }; +inline constexpr ::b2WorldTransform b2WorldTransform_identity = { { 0.0f, 0.0f }, { 1.0f, 0.0f } }; + +} // namespace mcpplibs::mwrap::box2d diff --git a/src/macros.cppm b/src/macros.cppm new file mode 100644 index 0000000..cd262d1 --- /dev/null +++ b/src/macros.cppm @@ -0,0 +1,137 @@ +// Generated by tools/gen_exports.py from the Box2D 3.2.0 headers — do not edit +// by hand. +// +// mcpplibs.mwrap.box2d.macros — the macros of that a C++ +// entity can be, under the original names in mcpplibs::mwrap::box2d::macros: +// +// - the constant object-like macros (B2_PI, B2_NULL_INDEX, B2_HASH_INIT, +// B2_MAX_POLYGON_VERTICES, B2_MAX_WORKERS, B2_DEFAULT_CATEGORY_BITS, +// B2_DEFAULT_MASK_BITS, B2_VERSION_* …) as `inline constexpr`, each of the +// type its macro expands to (B2_PI is a `float`, as 3.14159265359f is; +// B2_DEFAULT_MASK_BITS a `uint64_t`, as UINT64_MAX is); +// - the object-like macros that read b2_lengthUnitsPerMeter (B2_HUGE, +// B2_LINEAR_SLOP, B2_SPECULATIVE_DISTANCE, B2_CONTACT_RECYCLE_DISTANCE, +// B2_MAX_AABB_MARGIN). b2SetLengthUnitsPerMeter changes that variable at +// run time, so these are not constants: each is a function of no +// arguments returning the macro's value at the call, `B2_LINEAR_SLOP()`; +// - the function-like macros over ids (B2_IS_NULL, B2_IS_NON_NULL, +// B2_ID_EQUALS) as constexpr function templates. Each body is the macro +// itself, expanded on the parameters while the header's macros are +// defined (the declarator is parenthesised so the name is not expanded): +// the result has the macro's value and type for every id type. Each +// argument is evaluated once, before the call (the macro evaluates its +// arguments up to three times). +// +// Declaration specifiers, initializer syntax and the assertion macros stay +// macros, in , included before the import. +// tools/check_exports.py accounts for every macro. +module; +#include + +export module mcpplibs.mwrap.box2d.macros; + +// The header's values, taken while its macros are defined (module linkage). +namespace mwrap_box2d_capture { +inline constexpr auto value_B2_NULL_INDEX = B2_NULL_INDEX; +inline constexpr auto value_B2_ENABLE_VALIDATION = B2_ENABLE_VALIDATION; +inline constexpr auto value_B2_VERSION_MAJOR = B2_VERSION_MAJOR; +inline constexpr auto value_B2_VERSION_MINOR = B2_VERSION_MINOR; +inline constexpr auto value_B2_VERSION_REVISION = B2_VERSION_REVISION; +inline constexpr auto value_B2_HASH_INIT = B2_HASH_INIT; +inline constexpr auto value_B2_MAX_POLYGON_VERTICES = B2_MAX_POLYGON_VERTICES; +inline constexpr auto value_B2_MAX_WORKERS = B2_MAX_WORKERS; +inline constexpr auto value_B2_MAX_TASKS = B2_MAX_TASKS; +inline constexpr auto value_B2_GRAPH_COLOR_COUNT = B2_GRAPH_COLOR_COUNT; +inline constexpr auto value_B2_MAX_WORLDS = B2_MAX_WORLDS; +inline constexpr auto value_B2_NAME_LENGTH = B2_NAME_LENGTH; +inline constexpr auto value_B2_MAX_ROTATION = B2_MAX_ROTATION; +inline constexpr auto value_B2_CONTACT_RECYCLE_COS_ANGLE = B2_CONTACT_RECYCLE_COS_ANGLE; +inline constexpr auto value_B2_AABB_MARGIN_FRACTION = B2_AABB_MARGIN_FRACTION; +inline constexpr auto value_B2_TIME_TO_SLEEP = B2_TIME_TO_SLEEP; +inline constexpr auto value_B2_MAX_RESTITUTION_ITERATIONS = B2_MAX_RESTITUTION_ITERATIONS; +inline constexpr auto value_B2_PI = B2_PI; +inline constexpr auto value_B2_DEFAULT_CATEGORY_BITS = B2_DEFAULT_CATEGORY_BITS; +inline constexpr auto value_B2_DEFAULT_MASK_BITS = B2_DEFAULT_MASK_BITS; +inline auto value_B2_HUGE() noexcept { return B2_HUGE; } +inline auto value_B2_LINEAR_SLOP() noexcept { return B2_LINEAR_SLOP; } +inline auto value_B2_SPECULATIVE_DISTANCE() noexcept { return B2_SPECULATIVE_DISTANCE; } +inline auto value_B2_CONTACT_RECYCLE_DISTANCE() noexcept { return B2_CONTACT_RECYCLE_DISTANCE; } +inline auto value_B2_MAX_AABB_MARGIN() noexcept { return B2_MAX_AABB_MARGIN; } +} // namespace mwrap_box2d_capture + +// ── computation macros, defined while the header's macros are ────────────── +export namespace mcpplibs::mwrap::box2d::macros { + +// #define B2_IS_NULL(id) ( ( id ).index1 == 0 ) +template +constexpr auto (B2_IS_NULL)(T0 id) noexcept { return B2_IS_NULL(id); } +// #define B2_IS_NON_NULL(id) ( ( id ).index1 != 0 ) +template +constexpr auto (B2_IS_NON_NULL)(T0 id) noexcept { return B2_IS_NON_NULL(id); } +// #define B2_ID_EQUALS(id1, id2) ( ( id1 ).index1 == ( id2 ).index1 && ( id1 ).world0 == ( id2 ).world0 && ( id1 ).generation == ( id2 ).generation ) +template +constexpr auto (B2_ID_EQUALS)(T0 id1, T1 id2) noexcept { return B2_ID_EQUALS(id1, id2); } + +} // namespace mcpplibs::mwrap::box2d::macros + +// Every mapped object-like name, so the definitions below are not rewritten. +#undef B2_NULL_INDEX +#undef B2_ENABLE_VALIDATION +#undef B2_VERSION_MAJOR +#undef B2_VERSION_MINOR +#undef B2_VERSION_REVISION +#undef B2_HASH_INIT +#undef B2_MAX_POLYGON_VERTICES +#undef B2_MAX_WORKERS +#undef B2_MAX_TASKS +#undef B2_GRAPH_COLOR_COUNT +#undef B2_MAX_WORLDS +#undef B2_NAME_LENGTH +#undef B2_MAX_ROTATION +#undef B2_CONTACT_RECYCLE_COS_ANGLE +#undef B2_AABB_MARGIN_FRACTION +#undef B2_TIME_TO_SLEEP +#undef B2_MAX_RESTITUTION_ITERATIONS +#undef B2_PI +#undef B2_DEFAULT_CATEGORY_BITS +#undef B2_DEFAULT_MASK_BITS +#undef B2_HUGE +#undef B2_LINEAR_SLOP +#undef B2_SPECULATIVE_DISTANCE +#undef B2_CONTACT_RECYCLE_DISTANCE +#undef B2_MAX_AABB_MARGIN + +namespace c = mwrap_box2d_capture; + +export namespace mcpplibs::mwrap::box2d::macros { + +// ── constants ─────────────────────────────────────────────────────────────── +inline constexpr auto B2_NULL_INDEX = c::value_B2_NULL_INDEX; +inline constexpr auto B2_ENABLE_VALIDATION = c::value_B2_ENABLE_VALIDATION; +inline constexpr auto B2_VERSION_MAJOR = c::value_B2_VERSION_MAJOR; +inline constexpr auto B2_VERSION_MINOR = c::value_B2_VERSION_MINOR; +inline constexpr auto B2_VERSION_REVISION = c::value_B2_VERSION_REVISION; +inline constexpr auto B2_HASH_INIT = c::value_B2_HASH_INIT; +inline constexpr auto B2_MAX_POLYGON_VERTICES = c::value_B2_MAX_POLYGON_VERTICES; +inline constexpr auto B2_MAX_WORKERS = c::value_B2_MAX_WORKERS; +inline constexpr auto B2_MAX_TASKS = c::value_B2_MAX_TASKS; +inline constexpr auto B2_GRAPH_COLOR_COUNT = c::value_B2_GRAPH_COLOR_COUNT; +inline constexpr auto B2_MAX_WORLDS = c::value_B2_MAX_WORLDS; +inline constexpr auto B2_NAME_LENGTH = c::value_B2_NAME_LENGTH; +inline constexpr auto B2_MAX_ROTATION = c::value_B2_MAX_ROTATION; +inline constexpr auto B2_CONTACT_RECYCLE_COS_ANGLE = c::value_B2_CONTACT_RECYCLE_COS_ANGLE; +inline constexpr auto B2_AABB_MARGIN_FRACTION = c::value_B2_AABB_MARGIN_FRACTION; +inline constexpr auto B2_TIME_TO_SLEEP = c::value_B2_TIME_TO_SLEEP; +inline constexpr auto B2_MAX_RESTITUTION_ITERATIONS = c::value_B2_MAX_RESTITUTION_ITERATIONS; +inline constexpr auto B2_PI = c::value_B2_PI; +inline constexpr auto B2_DEFAULT_CATEGORY_BITS = c::value_B2_DEFAULT_CATEGORY_BITS; +inline constexpr auto B2_DEFAULT_MASK_BITS = c::value_B2_DEFAULT_MASK_BITS; + +// ── values that read b2_lengthUnitsPerMeter at the call ───────────────────── +inline auto B2_HUGE() noexcept { return c::value_B2_HUGE(); } +inline auto B2_LINEAR_SLOP() noexcept { return c::value_B2_LINEAR_SLOP(); } +inline auto B2_SPECULATIVE_DISTANCE() noexcept { return c::value_B2_SPECULATIVE_DISTANCE(); } +inline auto B2_CONTACT_RECYCLE_DISTANCE() noexcept { return c::value_B2_CONTACT_RECYCLE_DISTANCE(); } +inline auto B2_MAX_AABB_MARGIN() noexcept { return c::value_B2_MAX_AABB_MARGIN(); } + +} // namespace mcpplibs::mwrap::box2d::macros diff --git a/templates/physics/mcpp.toml.in b/templates/physics/mcpp.toml.in new file mode 100644 index 0000000..a637560 --- /dev/null +++ b/templates/physics/mcpp.toml.in @@ -0,0 +1,10 @@ +[package] +name = "{{project.name}}" +version = "0.1.0" + +[targets.{{project.name}}] +kind = "bin" +main = "src/main.cpp" + +[dependencies] +"{{self.name}}" = "{{self.version}}" diff --git a/templates/physics/src/main.cpp.in b/templates/physics/src/main.cpp.in new file mode 100644 index 0000000..9293f2b --- /dev/null +++ b/templates/physics/src/main.cpp.in @@ -0,0 +1,80 @@ +// {{project.name}} — 2D physics through import mcpplibs.mwrap.box2d: a tower +// of ten boxes on the ground is struck by a heavy ball, on a fixed 60 Hz step +// (4 sub-steps). Every half second it prints where the boxes are; at the end, +// the step on which everything fell asleep and the hash of every body's +// transform. Box2D is cross-platform deterministic: the hash is the same on +// Linux, macOS and Windows, x86_64 and arm64, with any worker count. +// +// {{project.name}} [seconds] simulate this long (default 4) +import std; +import mcpplibs.mwrap.box2d; + +namespace m = mcpplibs::mwrap::box2d::macros; // B2_HASH_INIT, B2_IS_NULL … + +int main(int argc, char** argv) { + int seconds = 4; + if (argc > 1 && std::from_chars(argv[1], argv[1] + std::strlen(argv[1]), seconds).ec != std::errc{}) { + std::println("usage: {{project.name}} [seconds]"); + return 2; + } + + b2WorldDef worldDef = b2DefaultWorldDef(); // gravity (0, -10) + b2WorldId world = b2CreateWorld(&worldDef); + + // The ground: a static box whose top is y = 0. + b2BodyDef groundDef = b2DefaultBodyDef(); + groundDef.position = b2Vec2{0.0f, -1.0f}; + b2BodyId ground = b2CreateBody(world, &groundDef); + b2Polygon groundBox = b2MakeBox(40.0f, 1.0f); + b2ShapeDef groundShape = b2DefaultShapeDef(); + b2CreatePolygonShape(ground, &groundShape, &groundBox); + + // A tower of ten 1 m boxes, and a ball thrown at it from the left. + std::vector bodies; + b2Polygon box = b2MakeBox(0.5f, 0.5f); + b2ShapeDef boxShape = b2DefaultShapeDef(); + for (int i = 0; i < 10; ++i) { + b2BodyDef def = b2DefaultBodyDef(); + def.type = b2_dynamicBody; + def.position = b2Vec2{0.0f, 0.5f + static_cast(i)}; + bodies.push_back(b2CreateBody(world, &def)); + b2CreatePolygonShape(bodies.back(), &boxShape, &box); + } + b2BodyDef ballDef = b2DefaultBodyDef(); + ballDef.type = b2_dynamicBody; + ballDef.position = b2Vec2{-12.0f, 6.0f}; + ballDef.linearVelocity = b2Vec2{16.0f, 2.0f}; + b2BodyId ball = b2CreateBody(world, &ballDef); + b2Circle circle = {b2Vec2{0.0f, 0.0f}, 0.5f}; + b2ShapeDef ballShape = b2DefaultShapeDef(); + ballShape.density = 10.0f; + b2CreateCircleShape(ball, &ballShape, &circle); + bodies.push_back(ball); + + const int steps = seconds * 60; + int sleepStep = -1; + for (int step = 1; step <= steps; ++step) { + b2World_Step(world, 1.0f / 60.0f, 4); + if (sleepStep < 0 && b2World_GetAwakeBodyCount(world) == 0) sleepStep = step; + if (step % 30 == 0) { + std::print("t={:4.1f}s", step / 60.0f); + for (std::size_t i = 0; i + 1 < bodies.size(); i += 3) { + b2Vec2 p = b2Body_GetPosition(bodies[i]); + std::print(" box{}=({:6.2f},{:5.2f})", i, p.x, p.y); + } + std::println(""); + } + } + + std::uint32_t hash = m::B2_HASH_INIT; + for (b2BodyId body : bodies) { + b2Transform t = b2Body_GetTransform(body); + hash = b2Hash(hash, reinterpret_cast(&t), static_cast(sizeof t)); + } + if (sleepStep < 0) + std::println("still moving after {} s; transform hash 0x{:08X}", seconds, hash); + else + std::println("asleep at step {}; transform hash 0x{:08X}", sleepStep, hash); + b2DestroyWorld(world); + return 0; +} diff --git a/templates/physics/template.toml b/templates/physics/template.toml new file mode 100644 index 0000000..c48e8c9 --- /dev/null +++ b/templates/physics/template.toml @@ -0,0 +1,4 @@ +[template] +description = "2D physics with import mcpplibs.mwrap.box2d: a tower of boxes struck by a ball on a fixed 60 Hz step, printed as it falls, and its final-state hash — the same bits on every platform Box2D supports" +default = true +post_message = "Run `mcpp run`, or `mcpp run -- ` to simulate longer." diff --git a/tests/api_test.cpp b/tests/api_test.cpp new file mode 100644 index 0000000..190ca62 --- /dev/null +++ b/tests/api_test.cpp @@ -0,0 +1,361 @@ +// The Box2D API through the module, spelled as documents it: +// the version, worlds and their definitions, bodies, shapes (polygon, circle, +// capsule, segment, chain), joints, contact / body / sensor events, the +// pre-solve and custom-filter callbacks, ray casts and overlap queries, the +// collision and geometry functions (manifolds, hulls, mass, distance), the +// inline math and id functions, the C++ vector operators, the length unit, +// the allocator and log hooks, and a custom task system. Only the module is +// imported; no Box2D header is included. +#include + +import std; +import mcpplibs.mwrap.box2d; + +using namespace mcpplibs::mwrap::box2d; // b2_nullBodyId, b2Vec2_zero, b2Rot_identity … +namespace m = mcpplibs::mwrap::box2d::macros; + +namespace { + +// A world with a static ground segment from x = -20 to 20 at y = 0. +struct World { + b2WorldId id; + b2BodyId ground; + explicit World(b2WorldDef def = b2DefaultWorldDef()) : id(b2CreateWorld(&def)) { + b2BodyDef groundDef = b2DefaultBodyDef(); + ground = b2CreateBody(id, &groundDef); + b2Segment segment = {{-20.0f, 0.0f}, {20.0f, 0.0f}}; + b2ShapeDef shapeDef = b2DefaultShapeDef(); + b2CreateSegmentShape(ground, &shapeDef, &segment); + } + ~World() { b2DestroyWorld(id); } + World(const World&) = delete; + World& operator=(const World&) = delete; + + b2BodyId box(b2Vec2 at, float half = 0.5f, bool events = false) { + b2BodyDef def = b2DefaultBodyDef(); + def.type = b2_dynamicBody; + def.position = at; + b2BodyId body = b2CreateBody(id, &def); + b2Polygon shape = b2MakeBox(half, half); + b2ShapeDef shapeDef = b2DefaultShapeDef(); + shapeDef.enableContactEvents = events; + b2CreatePolygonShape(body, &shapeDef, &shape); + return body; + } + void step(int count) { + for (int i = 0; i < count; ++i) b2World_Step(id, 1.0f / 60.0f, 4); + } +}; + +} // namespace + +TEST(Api, Version) { + b2Version v = b2GetVersion(); + EXPECT_EQ(v.major, m::B2_VERSION_MAJOR); + EXPECT_EQ(v.minor, m::B2_VERSION_MINOR); + EXPECT_EQ(v.revision, m::B2_VERSION_REVISION); + EXPECT_EQ(v.major * 100 + v.minor * 10 + v.revision, 320); + EXPECT_FALSE(b2IsDoublePrecision()); // compat.box2d: single precision + static_assert(std::is_same_v && std::is_same_v); +} + +TEST(Api, WorldDefinition) { + b2WorldDef def = b2DefaultWorldDef(); + EXPECT_EQ(def.gravity.y, -10.0f); + EXPECT_TRUE(def.enableSleep); + def.gravity = {0.0f, -9.8f}; + World world(def); + EXPECT_TRUE(b2World_IsValid(world.id)); + EXPECT_EQ(b2World_GetGravity(world.id), (b2Vec2{0.0f, -9.8f})); + b2World_SetGravity(world.id, b2Vec2{0.0f, -20.0f}); + EXPECT_EQ(b2World_GetGravity(world.id).y, -20.0f); + int data = 7; + b2World_SetUserData(world.id, &data); + EXPECT_EQ(b2World_GetUserData(world.id), &data); +} + +TEST(Api, FallRestSleep) { + World world; + b2BodyId body = world.box({0.0f, 4.0f}, 0.5f, true); + EXPECT_TRUE(b2Body_IsValid(body)); + EXPECT_EQ(b2Body_GetType(body), b2_dynamicBody); + b2MassData mass = b2Body_GetMassData(body); + EXPECT_NEAR(mass.mass, 1.0f, 1e-5f); // a 1x1 box of the default density 1 + + int begins = 0, moves = 0; + for (int i = 0; i < 240; ++i) { + world.step(1); + b2ContactEvents contacts = b2World_GetContactEvents(world.id); + begins += contacts.beginCount; + if (contacts.beginCount > 0) { + EXPECT_TRUE(b2Shape_IsValid(contacts.beginEvents[0].shapeIdA) && b2Shape_IsValid(contacts.beginEvents[0].shapeIdB)); + } + moves += b2World_GetBodyEvents(world.id).moveCount; + } + EXPECT_GE(begins, 1); + EXPECT_GT(moves, 0); + EXPECT_NEAR(b2Body_GetPosition(body).y, 0.5f, 0.01f); + EXPECT_FALSE(b2Body_IsAwake(body)); + b2Body_ApplyLinearImpulseToCenter(body, b2Vec2{0.0f, 5.0f}, true); + EXPECT_TRUE(b2Body_IsAwake(body)); + EXPECT_GT(b2Body_GetLinearVelocity(body).y, 4.0f); +} + +TEST(Api, NullIds) { + b2BodyId none = b2_nullBodyId; + EXPECT_TRUE(m::B2_IS_NULL(none)); + EXPECT_FALSE(b2Body_IsValid(none)); + World world; + EXPECT_TRUE(m::B2_IS_NON_NULL(world.ground)); + EXPECT_FALSE(m::B2_ID_EQUALS(world.ground, b2_nullBodyId)); + EXPECT_TRUE(m::B2_ID_EQUALS(world.ground, b2LoadBodyId(b2StoreBodyId(world.ground)))); + EXPECT_EQ(b2StoreWorldId(b2LoadWorldId(b2StoreWorldId(world.id))), b2StoreWorldId(world.id)); + EXPECT_TRUE(m::B2_IS_NULL(b2_nullWorldId) && m::B2_IS_NULL(b2_nullShapeId) && m::B2_IS_NULL(b2_nullJointId)); +} + +TEST(Api, ShapesAndQueries) { + World world; + b2BodyId body = world.box({0.0f, 0.5f}); + b2BodyDef def = b2DefaultBodyDef(); + def.position = {5.0f, 2.0f}; + b2BodyId ball = b2CreateBody(world.id, &def); + b2Circle circle = {{0.0f, 0.0f}, 0.5f}; + b2ShapeDef shapeDef = b2DefaultShapeDef(); + b2ShapeId circleShape = b2CreateCircleShape(ball, &shapeDef, &circle); + EXPECT_EQ(b2Shape_GetType(circleShape), b2_circleShape); + b2Capsule capsule = {{-1.0f, 0.0f}, {1.0f, 0.0f}, 0.25f}; + b2ShapeId capsuleShape = b2CreateCapsuleShape(ball, &shapeDef, &capsule); + EXPECT_EQ(b2Shape_GetType(capsuleShape), b2_capsuleShape); + EXPECT_EQ(b2Body_GetShapeCount(ball), 2); + + // The closest hit straight down from above the box is its top face. + b2RayResult hit = b2World_CastRayClosest(world.id, b2Vec2{0.0f, 10.0f}, b2Vec2{0.0f, -20.0f}, b2DefaultQueryFilter()); + ASSERT_TRUE(hit.hit); + EXPECT_TRUE(m::B2_ID_EQUALS(b2Shape_GetBody(hit.shapeId), body)); + EXPECT_NEAR(hit.point.y, 1.0f, 1e-4f); + EXPECT_EQ(hit.normal, (b2Vec2{0.0f, 1.0f})); + + // Every shape a ray crosses, through the callback. + struct Hits { int count = 0; } hits; + auto collect = [](b2ShapeId, b2Pos, b2Vec2, float, void* context) -> float { + ++static_cast(context)->count; + return 1.0f; // keep going + }; + b2World_CastRay(world.id, b2Vec2{-10.0f, 2.0f}, b2Vec2{20.0f, 0.0f}, b2DefaultQueryFilter(), collect, &hits); + EXPECT_EQ(hits.count, 2); // the circle and the capsule at y = 2 + + // An AABB overlap around the ball finds its two shapes. + int overlaps = 0; + auto count = [](b2ShapeId, void* context) -> bool { + ++*static_cast(context); + return true; + }; + b2AABB box = {{4.0f, 1.5f}, {6.0f, 2.5f}}; + b2World_OverlapAABB(world.id, b2Pos_zero, box, b2DefaultQueryFilter(), count, &overlaps); + EXPECT_EQ(overlaps, 2); +} + +TEST(Api, Chain) { + World world; + b2Vec2 points[] = {{-5.0f, 0.0f}, {0.0f, 1.0f}, {5.0f, 0.0f}, {0.0f, -1.0f}}; + b2SurfaceMaterial material = b2DefaultSurfaceMaterial(); + b2ChainDef def = b2DefaultChainDef(); + def.points = points; + def.pointCount = 4; + def.materials = &material; + def.materialCount = 1; + def.isLoop = true; + b2ChainId chain = b2CreateChain(world.ground, &def); + EXPECT_TRUE(b2Chain_IsValid(chain)); + EXPECT_EQ(b2Chain_GetSegmentCount(chain), 4); + b2DestroyChain(chain); + EXPECT_FALSE(b2Chain_IsValid(chain)); +} + +TEST(Api, RevoluteJoint) { + World world; + b2BodyId a = world.box({0.0f, 5.0f}); + b2BodyDef def = b2DefaultBodyDef(); + def.position = {0.0f, 8.0f}; + b2BodyId anchor = b2CreateBody(world.id, &def); // static + b2RevoluteJointDef joint = b2DefaultRevoluteJointDef(); + joint.base.bodyIdA = anchor; + joint.base.bodyIdB = a; + joint.base.localFrameA.p = {0.0f, 0.0f}; + joint.base.localFrameB.p = {0.0f, 3.0f}; + b2JointId id = b2CreateRevoluteJoint(world.id, &joint); + EXPECT_TRUE(b2Joint_IsValid(id)); + EXPECT_EQ(b2Joint_GetType(id), b2_revoluteJoint); + world.step(60); + // Hanging from the anchor: 3 m below it. + EXPECT_NEAR(b2Body_GetPosition(a).y, 5.0f, 0.05f); + b2DestroyJoint(id); + EXPECT_FALSE(b2Joint_IsValid(id)); +} + +TEST(Api, SensorEvents) { + World world; + b2BodyDef def = b2DefaultBodyDef(); + def.position = {0.0f, 2.0f}; + b2BodyId sensorBody = b2CreateBody(world.id, &def); + b2Polygon zone = b2MakeBox(2.0f, 0.5f); + b2ShapeDef sensorDef = b2DefaultShapeDef(); + sensorDef.isSensor = true; + sensorDef.enableSensorEvents = true; + b2CreatePolygonShape(sensorBody, &sensorDef, &zone); + + b2BodyDef boxDef = b2DefaultBodyDef(); + boxDef.type = b2_dynamicBody; + boxDef.position = {0.0f, 6.0f}; + b2BodyId body = b2CreateBody(world.id, &boxDef); + b2Polygon shape = b2MakeBox(0.25f, 0.25f); + b2ShapeDef shapeDef = b2DefaultShapeDef(); + shapeDef.enableSensorEvents = true; + b2CreatePolygonShape(body, &shapeDef, &shape); + + int begins = 0; + for (int i = 0; i < 120; ++i) { + world.step(1); + begins += b2World_GetSensorEvents(world.id).beginCount; + } + EXPECT_EQ(begins, 1); +} + +TEST(Api, Callbacks) { + World world; + world.box({0.0f, 0.5f}); + static int filtered = 0; + b2World_SetCustomFilterCallback(world.id, [](b2ShapeId, b2ShapeId, void*) -> bool { + ++filtered; + return true; + }, nullptr); + b2BodyDef def = b2DefaultBodyDef(); + def.type = b2_dynamicBody; + def.position = {0.0f, 2.0f}; + b2BodyId body = b2CreateBody(world.id, &def); + b2Polygon shape = b2MakeBox(0.5f, 0.5f); + b2ShapeDef shapeDef = b2DefaultShapeDef(); + shapeDef.enableCustomFiltering = true; + b2CreatePolygonShape(body, &shapeDef, &shape); + world.step(60); + EXPECT_GT(filtered, 0); +} + +TEST(Api, Geometry) { + b2Vec2 points[] = {{0, 0}, {1, 0}, {1, 1}, {0, 1}, {0.5f, 0.5f}}; + b2Hull hull = b2ComputeHull(points, 5); + EXPECT_EQ(hull.count, 4); + EXPECT_TRUE(b2ValidateHull(&hull)); + b2Polygon polygon = b2MakePolygon(&hull, 0.0f); + EXPECT_NEAR(b2ComputePolygonMass(&polygon, 2.0f).mass, 2.0f, 1e-5f); + EXPECT_LE(polygon.count, m::B2_MAX_POLYGON_VERTICES); + + // Two unit boxes, the second 0.9 above the first: overlapping by 0.1. + b2Polygon a = b2MakeBox(0.5f, 0.5f); + b2Transform xf = {{0.0f, 0.9f}, b2Rot_identity}; + b2LocalManifold manifold = b2CollidePolygons(&a, &a, xf); + EXPECT_EQ(manifold.pointCount, 2); + EXPECT_NEAR(manifold.normal.y, 1.0f, 1e-6f); + + b2SegmentDistanceResult d = b2ComputeSegmentDistance({0, 0}, {1, 0}, {0, 2}, {1, 2}); + EXPECT_NEAR(d.distanceSquared, 4.0f, 1e-6f); + + b2RayCastInput ray = {{-5.0f, 0.0f}, {10.0f, 0.0f}, 1.0f}; + b2CastOutput out = b2CastRayPolygon(&a, &ray); + EXPECT_TRUE(out.hit); + EXPECT_NEAR(out.fraction, 0.45f, 1e-6f); +} + +TEST(Api, InlineMathAndOperators) { + b2Vec2 a = {1.0f, 2.0f}, b = {3.0f, -1.0f}; + EXPECT_EQ(b2Add(a, b), a + b); + EXPECT_EQ(b2Sub(a, b), a - b); + EXPECT_EQ(b2MulSV(2.0f, a), 2.0f * a); + EXPECT_EQ(b2Neg(a), -a); + EXPECT_EQ(b2Dot(a, b), 1.0f); + EXPECT_EQ(b2Cross(a, b), -7.0f); + b2Vec2 c = a; + c += b; + c -= b; + c *= 1.0f; + EXPECT_TRUE(c == a && !(c != a)); + EXPECT_EQ(b2Length(b2Vec2{3.0f, 4.0f}), 5.0f); + EXPECT_EQ(b2MinInt(3, 4), 3); + EXPECT_EQ(b2ClampFloat(5.0f, 0.0f, 1.0f), 1.0f); + + // Box2D's own cosine and sine (the cross-platform deterministic ones). + b2Rot q = b2MakeRot(0.5f * m::B2_PI); + EXPECT_NEAR(q.c, 0.0f, 1e-3f); + EXPECT_NEAR(q.s, 1.0f, 1e-3f); + EXPECT_TRUE(b2IsNormalizedRot(q)); + b2CosSin cs = b2ComputeCosSin(0.0f); + EXPECT_EQ(cs.cosine, 1.0f); + EXPECT_EQ(cs.sine, 0.0f); + EXPECT_NEAR(b2Atan2(1.0f, 1.0f), 0.25f * m::B2_PI, 1e-4f); + b2Transform xf = {{1.0f, 0.0f}, b2MakeRot(0.0f)}; + EXPECT_EQ(b2TransformPoint(xf, b2Vec2{1.0f, 1.0f}), (b2Vec2{2.0f, 1.0f})); + EXPECT_EQ(b2InvTransformPoint(xf, b2Vec2{2.0f, 1.0f}), (b2Vec2{1.0f, 1.0f})); + EXPECT_EQ(b2Transform_identity.q.c, 1.0f); + EXPECT_EQ(b2Vec2_zero, (b2Vec2{0.0f, 0.0f})); + EXPECT_TRUE(b2IsValidVec2(a) && b2IsValidFloat(1.0f) && !b2IsValidFloat(std::numeric_limits::infinity())); +} + +TEST(Api, LengthUnits) { + EXPECT_EQ(b2GetLengthUnitsPerMeter(), 1.0f); + EXPECT_EQ(m::B2_LINEAR_SLOP(), 0.005f); + b2SetLengthUnitsPerMeter(100.0f); + EXPECT_EQ(b2_lengthUnitsPerMeter, 100.0f); + EXPECT_EQ(m::B2_LINEAR_SLOP(), 0.5f); + b2SetLengthUnitsPerMeter(1.0f); +} + +namespace { +std::atomic allocated{0}; +constexpr std::size_t kAlignment = 64; // at least any alignment Box2D asks for +void* allocate(std::size_t size, int alignment) { + if (static_cast(alignment) > kAlignment) return nullptr; + allocated += static_cast(size); + return ::operator new(size, std::align_val_t(kAlignment)); +} +void release(void* memory, std::size_t size) { + allocated -= static_cast(size); + ::operator delete(memory, std::align_val_t(kAlignment)); +} +} // namespace + +TEST(Api, Allocator) { + // b2SetAllocator reaches every allocation of a world made after it. + b2SetAllocator(allocate, release); + { + World world; + world.box({0.0f, 2.0f}); + world.step(10); + EXPECT_GT(allocated.load(), 0); + EXPECT_GT(b2GetByteCount(), 0u); + } + EXPECT_EQ(allocated.load(), 0); + b2SetAllocator(nullptr, nullptr); +} + +namespace { +// A task system that runs every task at once, on the caller. +int tasksRun = 0; +void* enqueue(b2TaskCallbackFcn* task, void* taskContext, void*) { + ++tasksRun; + task(taskContext); + return nullptr; // executed serially: nothing to finish +} +void finish(void*, void*) {} +} // namespace + +TEST(Api, TaskCallbacks) { + b2WorldDef def = b2DefaultWorldDef(); + def.workerCount = 4; + def.enqueueTask = enqueue; + def.finishTask = finish; + World world(def); + EXPECT_EQ(b2World_GetWorkerCount(world.id), 4); + for (int i = 0; i < 10; ++i) world.box({0.0f, 1.0f + 1.5f * i}); + world.step(60); + EXPECT_GT(tasksRun, 0); +} diff --git a/tests/determinism_test.cpp b/tests/determinism_test.cpp new file mode 100644 index 0000000..2858f56 --- /dev/null +++ b/tests/determinism_test.cpp @@ -0,0 +1,75 @@ +// Box2D's cross-platform determinism through the module: the same scene gives +// the same bits on every CI row (Linux x86_64 GCC and LLVM, Linux arm64, +// macOS arm64, Windows x86_64), with any worker count, through the built-in +// scheduler or a user task system, and on the SSE2 path where the CPU would +// take AVX2. The pinned values are the ones every row computes; upstream's +// falling-hinges values are upstream's own (shared/determinism.h). +// +// This translation unit is compiled with -ffp-contract=off (the package's +// cxxflags, as upstream compiles its tests): the falling-hinges setup does +// float arithmetic of its own (positions, b2MakeRot), which an FMA would +// change. The pyramid's setup is exact in binary and needs no flag. +#include + +import std; +import mcpplibs.mwrap.box2d; + +#include "../examples/box2d/src/scenes.hpp" + +namespace { + +using box2d_scenes::Result; +using box2d_scenes::Config; + +// Upstream's pins (single precision). +constexpr int kHingesSleepStep = 299; +constexpr std::uint32_t kHingesHash = 0x465F3DF4u; + +// This wrapper's pins: a 20-row pyramid, 600 steps. +constexpr std::uint32_t kPyramidTransforms = 0x790D7C28u; +constexpr std::uint32_t kPyramidEvents = 0xE8B80415u; +constexpr std::uint64_t kPyramidState = 0xE4E1D89FFCE4C43Eull; +constexpr int kPyramidSleepStep = -1; // sleep is off: it never falls asleep + +const Config kRuns[] = { + {1, false, false}, {2, false, false}, {4, false, false}, {8, false, false}, + {4, false, true}, {1, true, false}, {4, true, false}, +}; + +std::string describe(const Config& config) { + return std::format("workers={} sse2Fallback={} tasks={}", config.workers, config.sse2Fallback ? 1 : 0, + config.tasks ? "user" : "built-in"); +} + +void report(const char* scene, const Config& config, const Result& r) { + std::printf("determinism %s %s steps=%d sleepStep=%d transforms=0x%08X events=0x%08X state=0x%016llX\n", scene, + describe(config).c_str(), r.steps, r.sleepStep, r.transforms, r.events, + static_cast(r.state)); +} + +} // namespace + +TEST(Determinism, Platform) { + std::printf("determinism platform AVX2=%s\n", b2IsAVX2Available() ? "yes" : "no"); +} + +TEST(Determinism, FallingHingesUpstreamHash) { + for (const Config& config : kRuns) { + Result r = box2d_scenes::fallingHinges(config); + report("falling-hinges", config, r); + EXPECT_EQ(r.sleepStep, kHingesSleepStep) << describe(config); + EXPECT_EQ(r.transforms, kHingesHash) << describe(config); + } +} + +TEST(Determinism, Pyramid600Steps) { + for (const Config& config : kRuns) { + Result r = box2d_scenes::pyramid(config); + report("pyramid", config, r); + EXPECT_EQ(r.steps, 600); + EXPECT_EQ(r.sleepStep, kPyramidSleepStep) << describe(config); + EXPECT_EQ(r.transforms, kPyramidTransforms) << describe(config); + EXPECT_EQ(r.events, kPyramidEvents) << describe(config); + EXPECT_EQ(r.state, kPyramidState) << describe(config); + } +} diff --git a/tests/header_mix_test.cpp b/tests/header_mix_test.cpp new file mode 100644 index 0000000..46b062f --- /dev/null +++ b/tests/header_mix_test.cpp @@ -0,0 +1,68 @@ +// The raw header and the import in one translation unit (the side header +// first, the canonical order): the module's names are the very same entities +// as the header's; the macros a C++ entity cannot be work beside them — the +// declaration specifiers on declarations of one's own, the initializers, the +// assertions — and the headers' static constants sit beside the module's. +#include +#include + +import std; +import mcpplibs.mwrap.box2d; + +// Declarations of one's own in Box2D's style. +B2_API int mwrap_box2d_test_entry(int value); +int mwrap_box2d_test_entry(int value) { return value + 1; } + +B2_INLINE b2Vec2 mwrap_box2d_twice(b2Vec2 v) { return B2_LITERAL(b2Vec2){2.0f * v.x, 2.0f * v.y}; } + +struct B2_ALIGN_AS(16) Aligned { + float value; +}; + +TEST(HeaderMix, SameEntities) { + static_assert(std::is_same_v); + static_assert(std::is_same_v<::b2BodyId, b2BodyId> && sizeof(b2BodyId) == 8); + static_assert(alignof(Aligned) == 16); + EXPECT_EQ(&::b2World_Step, &b2World_Step); + EXPECT_EQ(&::b2_lengthUnitsPerMeter, &b2_lengthUnitsPerMeter); + EXPECT_EQ(::b2MakeRot(0.0f).c, 1.0f); // the header's inline function, the module's export + EXPECT_EQ(b2_dynamicBody, 2); // the header's enumerator + EXPECT_EQ(mwrap_box2d_test_entry(41), 42); + EXPECT_EQ(mwrap_box2d_twice(b2Vec2{1.0f, 2.0f}), (b2Vec2{2.0f, 4.0f})); +} + +TEST(HeaderMix, InitializersAndAssertions) { + b2BodyId none = B2_NULL_ID; + b2SimplexCache cache = B2_ZERO_INIT; + EXPECT_TRUE(B2_IS_NULL(none)); + EXPECT_EQ(cache.count, 0); + B2_ASSERT(B2_IS_NULL(none)); // holds: nothing is reported + B2_VALIDATE(cache.count == 0); + static int reported = 0; + b2SetAssertCallback([](const char*, const char*, int) -> int { + ++reported; + return 0; // no debugger break + }); +#if !defined(NDEBUG) || defined(B2_ENABLE_ASSERT) + B2_ASSERT(!B2_IS_NULL(none)); // fails: reported through the callback, with its file and line + EXPECT_EQ(reported, 1); + EXPECT_EQ(&::b2InternalAssert, &b2InternalAssert); +#endif +} + +TEST(HeaderMix, ConstantsBesideTheModules) { + namespace m = mcpplibs::mwrap::box2d::macros; + namespace b = mcpplibs::mwrap::box2d; + // An object-like macro expands even in a qualified name: set it aside to + // name the constant. + constexpr auto raw = B2_MAX_POLYGON_VERTICES; +#pragma push_macro("B2_MAX_POLYGON_VERTICES") +#undef B2_MAX_POLYGON_VERTICES + static_assert(m::B2_MAX_POLYGON_VERTICES == raw); +#pragma pop_macro("B2_MAX_POLYGON_VERTICES") + // A function-like macro is not expanded when its name is followed by `)`. + EXPECT_EQ(B2_ID_EQUALS(b2_nullBodyId, b::b2_nullBodyId), (m::B2_ID_EQUALS)(b2_nullBodyId, b::b2_nullBodyId)); + // The header's static constant and the module's: one value, two entities. + EXPECT_EQ(::b2Rot_identity.c, b::b2Rot_identity.c); + EXPECT_NE(static_cast(&::b2Rot_identity), static_cast(&b::b2Rot_identity)); +} diff --git a/tests/macros_mix_test.cpp b/tests/macros_mix_test.cpp new file mode 100644 index 0000000..d4c7659 --- /dev/null +++ b/tests/macros_mix_test.cpp @@ -0,0 +1,265 @@ +// Generated by tools/gen_exports.py from the Box2D 3.2.0 headers — do not edit +// by hand. +// +// Every mapping against the raw header, in one translation unit that includes +// (that is, ) beside the import: +// +// - each constant has its macro's type and value (the macro is set aside +// with push_macro around the comparison: while it is defined it expands +// even inside `m::NAME`); +// - each length-unit value gives the macro's value and type, at the default +// length unit and at another one set with b2SetLengthUnitsPerMeter; +// - each id macro function gives the macro's value and type for null and +// non-null ids of every id type, equal and unequal pairs; a function-like +// macro is not expanded when its name is followed by `)`, so +// `(m::NAME)(args)` calls the function beside the macro `NAME(args)`; +// - each `static const` object of the headers equals, byte for byte and in +// type, the module's constant of the same name. +#include +#include + +import std; +import mcpplibs.mwrap.box2d; + +namespace m = mcpplibs::mwrap::box2d::macros; +namespace b = mcpplibs::mwrap::box2d; + +// ── constants ─────────────────────────────────────────────────────────────── +constexpr auto raw_B2_NULL_INDEX = B2_NULL_INDEX; +constexpr auto raw_B2_ENABLE_VALIDATION = B2_ENABLE_VALIDATION; +constexpr auto raw_B2_VERSION_MAJOR = B2_VERSION_MAJOR; +constexpr auto raw_B2_VERSION_MINOR = B2_VERSION_MINOR; +constexpr auto raw_B2_VERSION_REVISION = B2_VERSION_REVISION; +constexpr auto raw_B2_HASH_INIT = B2_HASH_INIT; +constexpr auto raw_B2_MAX_POLYGON_VERTICES = B2_MAX_POLYGON_VERTICES; +constexpr auto raw_B2_MAX_WORKERS = B2_MAX_WORKERS; +constexpr auto raw_B2_MAX_TASKS = B2_MAX_TASKS; +constexpr auto raw_B2_GRAPH_COLOR_COUNT = B2_GRAPH_COLOR_COUNT; +constexpr auto raw_B2_MAX_WORLDS = B2_MAX_WORLDS; +constexpr auto raw_B2_NAME_LENGTH = B2_NAME_LENGTH; +constexpr auto raw_B2_MAX_ROTATION = B2_MAX_ROTATION; +constexpr auto raw_B2_CONTACT_RECYCLE_COS_ANGLE = B2_CONTACT_RECYCLE_COS_ANGLE; +constexpr auto raw_B2_AABB_MARGIN_FRACTION = B2_AABB_MARGIN_FRACTION; +constexpr auto raw_B2_TIME_TO_SLEEP = B2_TIME_TO_SLEEP; +constexpr auto raw_B2_MAX_RESTITUTION_ITERATIONS = B2_MAX_RESTITUTION_ITERATIONS; +constexpr auto raw_B2_PI = B2_PI; +constexpr auto raw_B2_DEFAULT_CATEGORY_BITS = B2_DEFAULT_CATEGORY_BITS; +constexpr auto raw_B2_DEFAULT_MASK_BITS = B2_DEFAULT_MASK_BITS; +#pragma push_macro("B2_NULL_INDEX") +#undef B2_NULL_INDEX +static_assert(std::is_same_v && m::B2_NULL_INDEX == raw_B2_NULL_INDEX); +#pragma pop_macro("B2_NULL_INDEX") +#pragma push_macro("B2_ENABLE_VALIDATION") +#undef B2_ENABLE_VALIDATION +static_assert(std::is_same_v && m::B2_ENABLE_VALIDATION == raw_B2_ENABLE_VALIDATION); +#pragma pop_macro("B2_ENABLE_VALIDATION") +#pragma push_macro("B2_VERSION_MAJOR") +#undef B2_VERSION_MAJOR +static_assert(std::is_same_v && m::B2_VERSION_MAJOR == raw_B2_VERSION_MAJOR); +#pragma pop_macro("B2_VERSION_MAJOR") +#pragma push_macro("B2_VERSION_MINOR") +#undef B2_VERSION_MINOR +static_assert(std::is_same_v && m::B2_VERSION_MINOR == raw_B2_VERSION_MINOR); +#pragma pop_macro("B2_VERSION_MINOR") +#pragma push_macro("B2_VERSION_REVISION") +#undef B2_VERSION_REVISION +static_assert(std::is_same_v && m::B2_VERSION_REVISION == raw_B2_VERSION_REVISION); +#pragma pop_macro("B2_VERSION_REVISION") +#pragma push_macro("B2_HASH_INIT") +#undef B2_HASH_INIT +static_assert(std::is_same_v && m::B2_HASH_INIT == raw_B2_HASH_INIT); +#pragma pop_macro("B2_HASH_INIT") +#pragma push_macro("B2_MAX_POLYGON_VERTICES") +#undef B2_MAX_POLYGON_VERTICES +static_assert(std::is_same_v && m::B2_MAX_POLYGON_VERTICES == raw_B2_MAX_POLYGON_VERTICES); +#pragma pop_macro("B2_MAX_POLYGON_VERTICES") +#pragma push_macro("B2_MAX_WORKERS") +#undef B2_MAX_WORKERS +static_assert(std::is_same_v && m::B2_MAX_WORKERS == raw_B2_MAX_WORKERS); +#pragma pop_macro("B2_MAX_WORKERS") +#pragma push_macro("B2_MAX_TASKS") +#undef B2_MAX_TASKS +static_assert(std::is_same_v && m::B2_MAX_TASKS == raw_B2_MAX_TASKS); +#pragma pop_macro("B2_MAX_TASKS") +#pragma push_macro("B2_GRAPH_COLOR_COUNT") +#undef B2_GRAPH_COLOR_COUNT +static_assert(std::is_same_v && m::B2_GRAPH_COLOR_COUNT == raw_B2_GRAPH_COLOR_COUNT); +#pragma pop_macro("B2_GRAPH_COLOR_COUNT") +#pragma push_macro("B2_MAX_WORLDS") +#undef B2_MAX_WORLDS +static_assert(std::is_same_v && m::B2_MAX_WORLDS == raw_B2_MAX_WORLDS); +#pragma pop_macro("B2_MAX_WORLDS") +#pragma push_macro("B2_NAME_LENGTH") +#undef B2_NAME_LENGTH +static_assert(std::is_same_v && m::B2_NAME_LENGTH == raw_B2_NAME_LENGTH); +#pragma pop_macro("B2_NAME_LENGTH") +#pragma push_macro("B2_MAX_ROTATION") +#undef B2_MAX_ROTATION +static_assert(std::is_same_v && m::B2_MAX_ROTATION == raw_B2_MAX_ROTATION); +#pragma pop_macro("B2_MAX_ROTATION") +#pragma push_macro("B2_CONTACT_RECYCLE_COS_ANGLE") +#undef B2_CONTACT_RECYCLE_COS_ANGLE +static_assert(std::is_same_v && m::B2_CONTACT_RECYCLE_COS_ANGLE == raw_B2_CONTACT_RECYCLE_COS_ANGLE); +#pragma pop_macro("B2_CONTACT_RECYCLE_COS_ANGLE") +#pragma push_macro("B2_AABB_MARGIN_FRACTION") +#undef B2_AABB_MARGIN_FRACTION +static_assert(std::is_same_v && m::B2_AABB_MARGIN_FRACTION == raw_B2_AABB_MARGIN_FRACTION); +#pragma pop_macro("B2_AABB_MARGIN_FRACTION") +#pragma push_macro("B2_TIME_TO_SLEEP") +#undef B2_TIME_TO_SLEEP +static_assert(std::is_same_v && m::B2_TIME_TO_SLEEP == raw_B2_TIME_TO_SLEEP); +#pragma pop_macro("B2_TIME_TO_SLEEP") +#pragma push_macro("B2_MAX_RESTITUTION_ITERATIONS") +#undef B2_MAX_RESTITUTION_ITERATIONS +static_assert(std::is_same_v && m::B2_MAX_RESTITUTION_ITERATIONS == raw_B2_MAX_RESTITUTION_ITERATIONS); +#pragma pop_macro("B2_MAX_RESTITUTION_ITERATIONS") +#pragma push_macro("B2_PI") +#undef B2_PI +static_assert(std::is_same_v && m::B2_PI == raw_B2_PI); +#pragma pop_macro("B2_PI") +#pragma push_macro("B2_DEFAULT_CATEGORY_BITS") +#undef B2_DEFAULT_CATEGORY_BITS +static_assert(std::is_same_v && m::B2_DEFAULT_CATEGORY_BITS == raw_B2_DEFAULT_CATEGORY_BITS); +#pragma pop_macro("B2_DEFAULT_CATEGORY_BITS") +#pragma push_macro("B2_DEFAULT_MASK_BITS") +#undef B2_DEFAULT_MASK_BITS +static_assert(std::is_same_v && m::B2_DEFAULT_MASK_BITS == raw_B2_DEFAULT_MASK_BITS); +#pragma pop_macro("B2_DEFAULT_MASK_BITS") + +// ── the static const objects ──────────────────────────────────────────────── +static_assert(std::is_same_v); +static_assert(std::is_same_v); +static_assert(std::is_same_v); +static_assert(std::is_same_v); +static_assert(std::is_same_v); +static_assert(std::is_same_v); +static_assert(std::is_same_v); +static_assert(std::is_same_v); +static_assert(std::is_same_v); +static_assert(std::is_same_v); +static_assert(std::is_same_v); +static_assert(std::is_same_v); +static_assert(std::is_same_v); + +TEST(StaticConstants, SameBytes) { + EXPECT_EQ(std::memcmp(&b::b2_emptySimplexCache, &::b2_emptySimplexCache, sizeof(::b2SimplexCache)), 0) << "b2_emptySimplexCache"; + EXPECT_EQ(std::memcmp(&b::b2_nullWorldId, &::b2_nullWorldId, sizeof(::b2WorldId)), 0) << "b2_nullWorldId"; + EXPECT_EQ(std::memcmp(&b::b2_nullBodyId, &::b2_nullBodyId, sizeof(::b2BodyId)), 0) << "b2_nullBodyId"; + EXPECT_EQ(std::memcmp(&b::b2_nullShapeId, &::b2_nullShapeId, sizeof(::b2ShapeId)), 0) << "b2_nullShapeId"; + EXPECT_EQ(std::memcmp(&b::b2_nullChainId, &::b2_nullChainId, sizeof(::b2ChainId)), 0) << "b2_nullChainId"; + EXPECT_EQ(std::memcmp(&b::b2_nullJointId, &::b2_nullJointId, sizeof(::b2JointId)), 0) << "b2_nullJointId"; + EXPECT_EQ(std::memcmp(&b::b2_nullContactId, &::b2_nullContactId, sizeof(::b2ContactId)), 0) << "b2_nullContactId"; + EXPECT_EQ(std::memcmp(&b::b2Vec2_zero, &::b2Vec2_zero, sizeof(::b2Vec2)), 0) << "b2Vec2_zero"; + EXPECT_EQ(std::memcmp(&b::b2Rot_identity, &::b2Rot_identity, sizeof(::b2Rot)), 0) << "b2Rot_identity"; + EXPECT_EQ(std::memcmp(&b::b2Transform_identity, &::b2Transform_identity, sizeof(::b2Transform)), 0) << "b2Transform_identity"; + EXPECT_EQ(std::memcmp(&b::b2Mat22_zero, &::b2Mat22_zero, sizeof(::b2Mat22)), 0) << "b2Mat22_zero"; + EXPECT_EQ(std::memcmp(&b::b2Pos_zero, &::b2Pos_zero, sizeof(::b2Pos)), 0) << "b2Pos_zero"; + EXPECT_EQ(std::memcmp(&b::b2WorldTransform_identity, &::b2WorldTransform_identity, sizeof(::b2WorldTransform)), 0) << "b2WorldTransform_identity"; +} + +namespace { + +// Null and non-null ids of every id type but b2WorldId (B2_ID_EQUALS does not +// apply to it upstream: it has no world0). +template +std::vector ids() { + std::vector out; + for (int index : {0, 1, 7, 0x7fffffff}) + for (int world : {0, 1, 3}) + for (int generation : {0, 1, 0xffff}) { + Id id{}; + id.index1 = index; + id.world0 = static_cast(world); + id.generation = static_cast(generation); + out.push_back(id); + } + return out; +} + +template +void unary(F check) { + for (auto v : ids()) check(v); + for (auto v : ids()) check(v); + for (auto v : ids()) check(v); + for (auto v : ids()) check(v); + for (auto v : ids()) check(v); + for (int index : {0, 1, 0xffff}) check(b2WorldId{static_cast(index), 1}); +} +template +void binary(F check) { + auto over = [&](const auto& values) { + for (auto a : values) + for (auto c : values) check(a, c); + }; + over(ids()); + over(ids()); + over(ids()); + over(ids()); +} + +// Restores the length unit a test changes. +struct LengthUnits { + explicit LengthUnits(float units) { b2SetLengthUnitsPerMeter(units); } + ~LengthUnits() { b2SetLengthUnitsPerMeter(1.0f); } +}; + +} // namespace + +// ── values that read b2_lengthUnitsPerMeter ───────────────────────────────── +TEST(LengthUnitValues, FollowTheLengthUnit) { + auto check = [] { + auto raw_B2_HUGE = B2_HUGE; +#pragma push_macro("B2_HUGE") +#undef B2_HUGE + static_assert(std::is_same_v); + EXPECT_EQ(m::B2_HUGE(), raw_B2_HUGE) << "B2_HUGE"; +#pragma pop_macro("B2_HUGE") + auto raw_B2_LINEAR_SLOP = B2_LINEAR_SLOP; +#pragma push_macro("B2_LINEAR_SLOP") +#undef B2_LINEAR_SLOP + static_assert(std::is_same_v); + EXPECT_EQ(m::B2_LINEAR_SLOP(), raw_B2_LINEAR_SLOP) << "B2_LINEAR_SLOP"; +#pragma pop_macro("B2_LINEAR_SLOP") + auto raw_B2_SPECULATIVE_DISTANCE = B2_SPECULATIVE_DISTANCE; +#pragma push_macro("B2_SPECULATIVE_DISTANCE") +#undef B2_SPECULATIVE_DISTANCE + static_assert(std::is_same_v); + EXPECT_EQ(m::B2_SPECULATIVE_DISTANCE(), raw_B2_SPECULATIVE_DISTANCE) << "B2_SPECULATIVE_DISTANCE"; +#pragma pop_macro("B2_SPECULATIVE_DISTANCE") + auto raw_B2_CONTACT_RECYCLE_DISTANCE = B2_CONTACT_RECYCLE_DISTANCE; +#pragma push_macro("B2_CONTACT_RECYCLE_DISTANCE") +#undef B2_CONTACT_RECYCLE_DISTANCE + static_assert(std::is_same_v); + EXPECT_EQ(m::B2_CONTACT_RECYCLE_DISTANCE(), raw_B2_CONTACT_RECYCLE_DISTANCE) << "B2_CONTACT_RECYCLE_DISTANCE"; +#pragma pop_macro("B2_CONTACT_RECYCLE_DISTANCE") + auto raw_B2_MAX_AABB_MARGIN = B2_MAX_AABB_MARGIN; +#pragma push_macro("B2_MAX_AABB_MARGIN") +#undef B2_MAX_AABB_MARGIN + static_assert(std::is_same_v); + EXPECT_EQ(m::B2_MAX_AABB_MARGIN(), raw_B2_MAX_AABB_MARGIN) << "B2_MAX_AABB_MARGIN"; +#pragma pop_macro("B2_MAX_AABB_MARGIN") + }; + check(); + LengthUnits pixels(32.0f); + check(); +} + +// ── computation macros ────────────────────────────────────────────────────── +TEST(MacroFunctions, B2_IS_NULL) { + unary([](auto id) { + static_assert(std::is_same_v); + EXPECT_EQ(B2_IS_NULL(id), (m::B2_IS_NULL)(id)); + }); +} +TEST(MacroFunctions, B2_IS_NON_NULL) { + unary([](auto id) { + static_assert(std::is_same_v); + EXPECT_EQ(B2_IS_NON_NULL(id), (m::B2_IS_NON_NULL)(id)); + }); +} +TEST(MacroFunctions, B2_ID_EQUALS) { + binary([](auto id1, auto id2) { + static_assert(std::is_same_v); + EXPECT_EQ(B2_ID_EQUALS(id1, id2), (m::B2_ID_EQUALS)(id1, id2)); + }); +} diff --git a/tests/macros_test.cpp b/tests/macros_test.cpp new file mode 100644 index 0000000..4e841fc --- /dev/null +++ b/tests/macros_test.cpp @@ -0,0 +1,50 @@ +// The macros module on its own (no header, no root module): the constants and +// the id functions are constant expressions, as the macros are, so they work +// in constant-initialised tables, array bounds and static_asserts; the +// length-unit values are functions of the length unit. +import std; +import mcpplibs.mwrap.box2d.macros; + +namespace macros = mcpplibs::mwrap::box2d::macros; +using namespace mcpplibs::mwrap::box2d::macros; + +// Every constant has the type its macro expands to. +static_assert(std::is_same_v); // 3.14159265359f +static_assert(std::is_same_v); // 8 +static_assert(std::is_same_v); // UINT64_MAX +static_assert(std::is_same_v); // 1 +static_assert(std::is_same_v); // ( -1 ) +static_assert(B2_PI == 3.14159265359f && B2_MAX_ROTATION == 0.25f * B2_PI); +static_assert(B2_VERSION_MAJOR == 3 && B2_VERSION_MINOR == 2 && B2_VERSION_REVISION == 0); +static_assert(B2_HASH_INIT == 5381 && B2_NULL_INDEX == -1); +static_assert(B2_MAX_WORKERS == 32 && B2_MAX_TASKS == 256 && B2_GRAPH_COLOR_COUNT == 24); +static_assert(B2_MAX_WORLDS == 128 && B2_NAME_LENGTH == 10 && B2_MAX_RESTITUTION_ITERATIONS == 63); +static_assert(B2_DEFAULT_MASK_BITS == ~std::uint64_t{0} && B2_DEFAULT_CATEGORY_BITS == 1); +static_assert(B2_TIME_TO_SLEEP == 0.5f && B2_AABB_MARGIN_FRACTION == 0.125f && B2_CONTACT_RECYCLE_COS_ANGLE == 0.98f); + +// The id functions, at compile time, on any struct with the id members. +struct Id { + std::int32_t index1; + std::uint16_t world0; + std::uint16_t generation; +}; +static_assert(B2_IS_NULL(Id{0, 1, 1}) && !B2_IS_NULL(Id{1, 0, 0})); +static_assert(B2_IS_NON_NULL(Id{3, 0, 0}) && !B2_IS_NON_NULL(Id{})); +static_assert(B2_ID_EQUALS(Id{1, 2, 3}, Id{1, 2, 3}) && !B2_ID_EQUALS(Id{1, 2, 3}, Id{1, 2, 4})); +static_assert(std::is_same_v); + +// A constant-initialised table and array bounds, as the macros allow. +struct Polygon { + float vertices[2 * B2_MAX_POLYGON_VERTICES]; + int count; +}; +constexpr Polygon square = {{-1, -1, 1, -1, 1, 1, -1, 1}, 4}; +static_assert(square.count < B2_MAX_POLYGON_VERTICES && std::size(square.vertices) == 16); + +int main() { + // At the default length unit (1 per meter): upstream's documented values. + if (B2_LINEAR_SLOP() != 0.005f || B2_SPECULATIVE_DISTANCE() != 4.0f * 0.005f) return 1; + if (B2_HUGE() != 100.0f * 1000.0f || B2_MAX_AABB_MARGIN() != 0.05f) return 2; + if (B2_CONTACT_RECYCLE_DISTANCE() != 10.0f * 0.005f) return 3; + return macros::B2_VERSION_MAJOR == 3 && macros::B2_MAX_POLYGON_VERTICES == 8 ? 0 : 4; +} diff --git a/tools/candidate_index.py b/tools/candidate_index.py new file mode 100644 index 0000000..30b831e --- /dev/null +++ b/tools/candidate_index.py @@ -0,0 +1,85 @@ +#!/usr/bin/env python3 +"""The compat namespace from mcpp-index's candidate branch, while the published +index does not yet carry compat.box2d. + +compat.box2d is added to mcpp-index by mcpplibs/mcpp-index#611. Until that +descriptor is on the index's main branch, a build of this repository cannot +resolve its one dependency from the published index. This tool then clones +the PR's branch, prepares the unchanged namespace views of its descriptors +(the index's own tests/prepare_mwrap_indices.py) and prints the checkout. + + --checkout-only print the candidate checkout (nothing once published), + for tools/test_template.py --index + --ci also append `[indices] compat = { path = }` to this + checkout's manifests (the root and examples/box2d), in + CI's working copy only + +Once the published index has compat.box2d, it does nothing. +""" +import argparse +from pathlib import Path +import shutil +import subprocess +import sys +import time +import urllib.error +import urllib.request + +INDEX = 'https://github.com/mcpplibs/mcpp-index' +BRANCH = 'compat-box2d' +PUBLISHED = 'https://raw.githubusercontent.com/mcpplibs/mcpp-index/main/pkgs/c/compat.box2d.lua' +MANIFESTS = ['mcpp.toml', 'examples/box2d/mcpp.toml'] + + +def published() -> bool: + for attempt in range(4): + try: + with urllib.request.urlopen(PUBLISHED, timeout=60) as response: + return response.status == 200 + except urllib.error.HTTPError as error: + if error.code == 404: + return False + raise + except urllib.error.URLError: + if attempt == 3: + raise + time.sleep(10) + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + mode = parser.add_mutually_exclusive_group(required=True) + mode.add_argument('--checkout-only', action='store_true') + mode.add_argument('--ci', action='store_true') + args = parser.parse_args() + root = Path(__file__).resolve().parents[1] + if published(): + print('compat.box2d is published: the published index is used', file=sys.stderr) + return + checkout = root / 'target/candidate-index' + for attempt in range(4): + if (checkout / 'index.toml').exists(): + break + shutil.rmtree(checkout, ignore_errors=True) + clone = subprocess.run(['git', 'clone', '--quiet', '--depth', '1', '--branch', BRANCH, INDEX + '.git', + str(checkout)], stdout=sys.stderr) + if clone.returncode != 0 and attempt == 3: + sys.exit(f'could not clone {INDEX} {BRANCH}') + if clone.returncode != 0: + time.sleep(10) + subprocess.run([sys.executable, str(checkout / 'tests/prepare_mwrap_indices.py')], cwd=checkout, check=True, + stdout=sys.stderr) + view = checkout / 'tests/mwrap/.indices/compat' + if not (view / 'pkgs/view/compat.box2d.lua').exists(): + sys.exit(f'{INDEX} {BRANCH} has no compat.box2d') + if args.ci: + for manifest in MANIFESTS: + with (root / manifest).open('a', encoding='utf-8', newline='\n') as out: + out.write('\n# CI only (tools/candidate_index.py): compat.box2d from ' + BRANCH + '\n') + out.write('[indices]\ncompat = { path = "' + view.as_posix() + '" }\n') + print('candidate compat index for', manifest, file=sys.stderr) + print(checkout.as_posix()) + + +if __name__ == '__main__': + main() diff --git a/tools/check_exports.py b/tools/check_exports.py new file mode 100644 index 0000000..9a51a8a --- /dev/null +++ b/tools/check_exports.py @@ -0,0 +1,89 @@ +#!/usr/bin/env python3 +"""Audit the module surface against the pinned release's public Box2D headers. + +- src/box2d.cppm, src/macros.cppm and tests/macros_mix_test.cpp are what + tools/gen_exports.py writes. +- Every type, opaque struct, enumerator, B2_API function, inline function, + C++ operator and variable of the installed headers is exported by + `export using`, and nothing else is. +- Every `static const` object of the headers is a constant of the same type + in mcpplibs::mwrap::box2d, checked byte for byte against the header's by + tests/macros_mix_test.cpp. +- Every macro of the headers is mapped in src/macros.cppm (a constant, a + length-unit function or a constexpr function), or kept as a macro in + with its reason; and each mapping is checked + against the raw header by tests/macros_mix_test.cpp. +""" +from pathlib import Path +import re +import subprocess +import sys + +sys.path.insert(0, str(Path(__file__).resolve().parent)) +from inventory import fetch, surface # noqa: E402 +from gen_exports import SIDE_HEADER # noqa: E402 + + +def mapped_names(text: str) -> set[str]: + text = text[text.index('export module'):] + names = set(re.findall(r'^inline constexpr auto (\w+) = c::value_\1;', text, flags=re.M)) + names |= set(re.findall(r'^inline auto (\w+)\(\) noexcept \{ return c::value_\1\(\); \}', text, flags=re.M)) + names |= set(re.findall(r'^constexpr auto \((\w+)\)\(', text, flags=re.M)) + return names + + +def main(): + root = Path(__file__).resolve().parents[1] + problems = [] + generated = subprocess.run([sys.executable, str(root / 'tools/gen_exports.py'), '--check'], capture_output=True, text=True) + if generated.returncode != 0: + problems.append(generated.stderr.strip() or generated.stdout.strip()) + sf = surface(fetch(root)) + module = (root / 'src/box2d.cppm').read_text(encoding='utf-8') + exported = set(re.findall(r'^export using ::(\w+|operator\S+);', module, flags=re.M)) + declared = (set(sf.types) | set(sf.opaque) | set(sf.enumerators) | set(sf.functions) | set(sf.inline) + | set(sf.operators) | set(sf.variables)) + if declared - exported: + problems.append('src/box2d.cppm lacks: ' + ', '.join(sorted(declared - exported))) + if exported - declared: + problems.append('src/box2d.cppm exports what the headers do not declare: ' + ', '.join(sorted(exported - declared))) + + constants = {m.group(2): m.group(1) + for m in re.finditer(r'^inline constexpr ::(\w+) (\w+) = ', module, flags=re.M)} + if constants != {c.name: c.type for c in sf.constants}: + problems.append(f'static constants: module {sorted(constants.items())} != headers ' + f'{sorted((c.name, c.type) for c in sf.constants)}') + + macros_text = (root / 'src/macros.cppm').read_text(encoding='utf-8') + mapped = mapped_names(macros_text) + macros = set(sf.macros) + unaccounted = macros - mapped - SIDE_HEADER.keys() + if unaccounted: + problems.append('macros neither mapped nor side-header: ' + ', '.join(sorted(unaccounted))) + both = SIDE_HEADER.keys() & mapped + if both: + problems.append('listed as side-header yet mapped: ' + ', '.join(sorted(both))) + stale = SIDE_HEADER.keys() - macros + if stale: + problems.append('listed but not in the headers: ' + ', '.join(sorted(stale))) + test = (root / 'tests/macros_mix_test.cpp').read_text(encoding='utf-8') + untested = {n for n in mapped if f'm::{n}' not in test} | {n for n in constants if f'b::{n}' not in test} + if untested: + problems.append('mapped but not checked against the header: ' + ', '.join(sorted(untested))) + side = (root / 'include/mwrap/box2d/macros.hpp').read_text(encoding='utf-8') + if '#include ' not in side: + problems.append(' does not include ') + if problems: + sys.exit('\n'.join(problems)) + functions = sum(1 for m in sf.macros.values() if m.name in mapped and m.params is not None) + runtime = len(re.findall(r'^inline auto (\w+)\(\) noexcept \{ return c::value_', macros_text, flags=re.M)) + print(f'{len(sf.types)} types, {len(sf.opaque)} opaque structs, {len(sf.enumerators)} enumerators, ' + f'{len(sf.functions)} B2_API functions ({len(sf.guarded)} under the assertion conditional), ' + f'{len(sf.inline)} inline functions, {len(sf.operators)} operators and {len(sf.variables)} variable exported; ' + f'{len(constants)} static constants as constants of their types; {len(macros)} macros: ' + f'{len(mapped) - functions - runtime} constants, {runtime} length-unit functions and {functions} functions ' + f'mapped (each checked against the header), {len(SIDE_HEADER)} kept as macros') + + +if __name__ == '__main__': + main() diff --git a/tools/check_mwrap_release.py b/tools/check_mwrap_release.py new file mode 100644 index 0000000..9bedddb --- /dev/null +++ b/tools/check_mwrap_release.py @@ -0,0 +1,50 @@ +#!/usr/bin/env python3 +"""Check the immutable mwrap release coordinate and bilingual entry pages.""" +import argparse +import hashlib +import json +from pathlib import Path +import re +import tomllib + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--tag', help='release tag; omitted during PR checks') + args = parser.parse_args() + root = Path(__file__).resolve().parents[1] + package = tomllib.loads((root / 'mcpp.toml').read_text(encoding="utf-8"))['package'] + assert package['namespace'] == 'mcpplibs.mwrap', package + assert re.fullmatch(r'[A-Za-z_][A-Za-z_0-9]*', package['name']), package['name'] + upstream = package['metadata']['mwrap']['upstream-version'] + version = package['version'] + assert version == upstream or re.fullmatch(re.escape(upstream) + r'\.[1-9][0-9]*', version), (version, upstream) + if args.tag: + assert args.tag == 'mwrap-' + version, (args.tag, version) + assert package['license'] == 'Apache-2.0', package['license'] + assert package['authors'] == ['mcpp-community'], package.get('authors') + license_text = (root / 'LICENSE').read_text(encoding='utf-8') + assert 'Apache License' in license_text and 'Version 2.0' in license_text + assert 'mcpp-community' in (root / 'NOTICE').read_text(encoding='utf-8') + documents = json.loads((root / 'upstream/documents.json').read_text(encoding='utf-8'))['documents'] + assert any(Path(d['path']).name.startswith('README') for d in documents) + assert any(Path(d['path']).name.startswith(('LICENSE', 'COPYING')) for d in documents) + deployed = {entry['from'] for entry in tomllib.loads((root / 'mcpp.toml').read_text(encoding='utf-8')).get('runtime', {}).get('deploy', [])} + assert {'LICENSE', 'NOTICE'} <= deployed + for document in documents: + path = Path(document['path']) + assert not path.is_absolute() and '..' not in path.parts and path.parts[0] == 'upstream' + assert hashlib.sha256((root / path).read_bytes()).hexdigest() == document['sha256'], path + assert document['source'] and path.as_posix() in deployed, path + identity = package['namespace'] + '.' + package['name'] + for name, other in [('README.md', 'README.zh.md'), ('README.zh.md', 'README.md')]: + text = (root / name).read_text(encoding="utf-8") + assert len(text.splitlines()) <= 150, name + assert '](' + other + ')' in '\n'.join(text.splitlines()[:12]), name + assert identity in text and version in text, name + assert text.count('```') % 2 == 0, name + print(identity + '@' + version) + + +if __name__ == '__main__': + main() diff --git a/tools/gen_exports.py b/tools/gen_exports.py new file mode 100644 index 0000000..eeafe31 --- /dev/null +++ b/tools/gen_exports.py @@ -0,0 +1,363 @@ +#!/usr/bin/env python3 +"""Write the module sources and the generated macro test from the Box2D headers. + +- src/box2d.cppm exports, by `export using` and under their upstream global + names, every type (structs, enums, function types, aliases), opaque struct, + enumerator, B2_API function, inline function (the B2_INLINE math and id + functions are `inline`, external linkage, in C++), C++ vector operator and + the one exported variable (b2_lengthUnitsPerMeter) of the installed + headers. Its `static const` objects (b2Vec2_zero, b2Rot_identity, + b2_nullBodyId …) have internal linkage and cannot be exported as they are: + each is an `inline constexpr` of the same type and initializer in + mcpplibs::mwrap::box2d, under the same name. It re-exports the macros module. +- src/macros.cppm holds, in mcpplibs::mwrap::box2d::macros under their own + names: + * the constant object-like macros (B2_PI, B2_MAX_POLYGON_VERTICES, + B2_HASH_INIT, B2_DEFAULT_MASK_BITS, B2_VERSION_* …) as `inline + constexpr` of the type of the macro's own expansion; + * the object-like macros that read b2_lengthUnitsPerMeter (B2_HUGE, + B2_LINEAR_SLOP, B2_SPECULATIVE_DISTANCE, B2_CONTACT_RECYCLE_DISTANCE, + B2_MAX_AABB_MARGIN): that variable is set at run time by + b2SetLengthUnitsPerMeter, so they are not constants. Each is an inline + function of no arguments returning the macro's value, `B2_LINEAR_SLOP()`; + * the function-like macros over ids (B2_IS_NULL, B2_IS_NON_NULL, + B2_ID_EQUALS) as constexpr function templates whose body is the macro. +- tests/macros_mix_test.cpp checks each mapping against the raw header in a + translation unit that includes it beside the import. + +With --check, nothing is written and a difference fails. +""" +import argparse +from pathlib import Path +import re +import sys + +sys.path.insert(0, str(Path(__file__).resolve().parent)) +from inventory import GUARD as ASSERT_GUARD, fetch, surface # noqa: E402 + +# Macros that stay macros, in (which is ). +SIDE_HEADER = { + 'BOX2D_EXPORT': 'a declaration attribute (dllexport/dllimport/visibility; empty for the static library)', + 'B2_API': 'a declaration specifier (extern "C" and BOX2D_EXPORT)', + 'B2_INLINE': 'a declaration specifier (inline in C++, static inline in C)', + 'B2_FORCE_INLINE': 'a declaration specifier (inline with always_inline / __forceinline)', + 'B2_ID_INLINE': 'a declaration specifier (inline in C++, static inline in C)', + 'B2_ALIGN_AS': 'an alignment specifier (alignas / _Alignas)', + 'B2_LITERAL': 'C compound-literal syntax: (T) in C, T in C++', + 'B2_ZERO_INIT': 'a braced initializer ({} in C++), not a value of one type', + 'B2_NULL_ID': 'a braced initializer ({} in C++) for any id type, not a value of one type', + 'B2_BREAKPOINT': 'a statement that traps into the debugger (__debugbreak / __builtin_trap)', + 'B2_ASSERT': 'an assertion: stringifies its condition and reports __FILE__ / __LINE__ of the call site', + 'B2_VALIDATE': 'an assertion in validation builds (B2_ASSERT), nothing otherwise', +} + +RUNTIME = 'b2_lengthUnitsPerMeter' + +GUARD = '#include ' + + +def classify(sf): + """(constants, runtime, functions): the mapped macros, in header order.""" + constants, runtime, functions = [], [], [] + for macro in sf.macros.values(): + if macro.name in SIDE_HEADER: + continue + if macro.params is not None: + functions.append(macro) + elif reads_runtime(sf, macro.name): + runtime.append(macro.name) + else: + constants.append(macro.name) + return constants, runtime, functions + + +def reads_runtime(sf, name: str, seen=()) -> bool: + body = sf.macros[name].body + if re.search(r'\b' + RUNTIME + r'\b', body): + return True + return any(reads_runtime(sf, n, seen + (name,)) for n in re.findall(r'\b\w+\b', body) + if n in sf.macros and n not in seen and n != name) + + +ROOT_HEADER = '''// Generated by tools/gen_exports.py from the Box2D 3.2.0 headers — do not edit +// by hand. +// +// mcpplibs.mwrap.box2d — Box2D v3, the 2D rigid-body physics engine for games, +// as a C++ module. Every exported name is the upstream global of +// , unchanged: the types, enums and their enumerators, every +// B2_API function (worlds, bodies, shapes, chains, joints, contacts, events, +// queries, the collision and geometry functions, recording and replay), the +// inline math and id functions, the C++ vector operators and +// b2_lengthUnitsPerMeter. The headers' `static const` objects (internal +// linkage) are constants of the same type and value in mcpplibs::mwrap::box2d. +// The root also re-exports the macro mappings. The library is compat.box2d, +// built from the same release (single precision, upstream's default SIMD, +// -ffp-contract=off). +module; +''' + GUARD + ''' + +export module mcpplibs.mwrap.box2d; + +export import mcpplibs.mwrap.box2d.macros; +''' + + +def render_root(sf) -> str: + out = [ROOT_HEADER] + out.append(f'// types: structs, enums, function types and aliases ({len(sf.types)})') + out += [f'export using ::{n};' for n in sf.types] + out.append(f'\n// opaque structs, declared and not defined ({len(sf.opaque)})') + out += [f'export using ::{n};' for n in sf.opaque] + by_enum: dict[str, list[str]] = {} + for e, t in sf.enumerators.items(): + by_enum.setdefault(t, []).append(e) + out.append(f'\n// enumerators ({len(sf.enumerators)})') + for t, names in by_enum.items(): + out.append(f'// {t} ({len(names)})') + out += [f'export using ::{n};' for n in names] + plain = [n for n in sf.functions if n not in sf.guarded] + out.append(f'\n// B2_API functions ({len(sf.functions)})') + out += [f'export using ::{n};' for n in plain] + for n in sorted(sf.guarded): + out += [f'// declared, and defined by the library, only under {ASSERT_GUARD}', + f'#if {ASSERT_GUARD}', f'export using ::{n};', '#endif'] + out.append(f'\n// inline functions: B2_INLINE, B2_FORCE_INLINE and B2_ID_INLINE are `inline` in C++ ({len(sf.inline)})') + out += [f'export using ::{n};' for n in sf.inline] + out.append(f'\n// the C++ vector operators of math_functions.h ({len(sf.operators)})') + out += [f'export using ::{n};' for n in sf.operators] + out.append(f'\n// variables ({len(sf.variables)})') + out += [f'export using ::{n};' for n in sf.variables] + out.append(f''' +// The headers' `static const` objects ({len(sf.constants)}) have internal linkage: a +// named module cannot export them. Each is a constant of the same type and +// initializer here, under the same name. +export namespace mcpplibs::mwrap::box2d {{ +''') + out += [f'inline constexpr ::{c.type} {c.name} = {c.init};' for c in sf.constants] + out.append('\n} // namespace mcpplibs::mwrap::box2d') + return '\n'.join(out) + '\n' + + +MACROS_HEADER = '''// Generated by tools/gen_exports.py from the Box2D 3.2.0 headers — do not edit +// by hand. +// +// mcpplibs.mwrap.box2d.macros — the macros of that a C++ +// entity can be, under the original names in mcpplibs::mwrap::box2d::macros: +// +// - the constant object-like macros (B2_PI, B2_NULL_INDEX, B2_HASH_INIT, +// B2_MAX_POLYGON_VERTICES, B2_MAX_WORKERS, B2_DEFAULT_CATEGORY_BITS, +// B2_DEFAULT_MASK_BITS, B2_VERSION_* …) as `inline constexpr`, each of the +// type its macro expands to (B2_PI is a `float`, as 3.14159265359f is; +// B2_DEFAULT_MASK_BITS a `uint64_t`, as UINT64_MAX is); +// - the object-like macros that read b2_lengthUnitsPerMeter (B2_HUGE, +// B2_LINEAR_SLOP, B2_SPECULATIVE_DISTANCE, B2_CONTACT_RECYCLE_DISTANCE, +// B2_MAX_AABB_MARGIN). b2SetLengthUnitsPerMeter changes that variable at +// run time, so these are not constants: each is a function of no +// arguments returning the macro's value at the call, `B2_LINEAR_SLOP()`; +// - the function-like macros over ids (B2_IS_NULL, B2_IS_NON_NULL, +// B2_ID_EQUALS) as constexpr function templates. Each body is the macro +// itself, expanded on the parameters while the header's macros are +// defined (the declarator is parenthesised so the name is not expanded): +// the result has the macro's value and type for every id type. Each +// argument is evaluated once, before the call (the macro evaluates its +// arguments up to three times). +// +// Declaration specifiers, initializer syntax and the assertion macros stay +// macros, in , included before the import. +// tools/check_exports.py accounts for every macro. +module; +''' + GUARD + ''' +''' + + +def function(macro) -> list[str]: + tparams = ', '.join(f'class T{i}' for i in range(len(macro.params))) + params = ', '.join(f'T{i} {p}' for i, p in enumerate(macro.params)) + call = f'{macro.name}({", ".join(macro.params)})' + return [f'template <{tparams}>', f'constexpr auto ({macro.name})({params}) noexcept {{ return {call}; }}'] + + +def render_macros(sf) -> str: + constants, runtime, functions = classify(sf) + out = [MACROS_HEADER] + out.append('export module mcpplibs.mwrap.box2d.macros;\n') + out.append('// The header\'s values, taken while its macros are defined (module linkage).') + out.append('namespace mwrap_box2d_capture {') + out += [f'inline constexpr auto value_{n} = {n};' for n in constants] + out += [f'inline auto value_{n}() noexcept {{ return {n}; }}' for n in runtime] + out.append('} // namespace mwrap_box2d_capture\n') + out.append('// ── computation macros, defined while the header\'s macros are ──────────────') + out.append('export namespace mcpplibs::mwrap::box2d::macros {\n') + for macro in functions: + out.append(f'// #define {macro.name}({", ".join(macro.params)}) {macro.body}') + out += function(macro) + out.append('\n} // namespace mcpplibs::mwrap::box2d::macros\n') + out.append('// Every mapped object-like name, so the definitions below are not rewritten.') + out += [f'#undef {n}' for n in constants + runtime] + out.append('\nnamespace c = mwrap_box2d_capture;\n') + out.append('export namespace mcpplibs::mwrap::box2d::macros {\n') + out.append('// ── constants ───────────────────────────────────────────────────────────────') + out += [f'inline constexpr auto {n} = c::value_{n};' for n in constants] + out.append('\n// ── values that read b2_lengthUnitsPerMeter at the call ─────────────────────') + out += [f'inline auto {n}() noexcept {{ return c::value_{n}(); }}' for n in runtime] + out.append('\n} // namespace mcpplibs::mwrap::box2d::macros') + return '\n'.join(out) + '\n' + + +TEST_HEADER = '''// Generated by tools/gen_exports.py from the Box2D 3.2.0 headers — do not edit +// by hand. +// +// Every mapping against the raw header, in one translation unit that includes +// (that is, ) beside the import: +// +// - each constant has its macro's type and value (the macro is set aside +// with push_macro around the comparison: while it is defined it expands +// even inside `m::NAME`); +// - each length-unit value gives the macro's value and type, at the default +// length unit and at another one set with b2SetLengthUnitsPerMeter; +// - each id macro function gives the macro's value and type for null and +// non-null ids of every id type, equal and unequal pairs; a function-like +// macro is not expanded when its name is followed by `)`, so +// `(m::NAME)(args)` calls the function beside the macro `NAME(args)`; +// - each `static const` object of the headers equals, byte for byte and in +// type, the module's constant of the same name. +#include +#include + +import std; +import mcpplibs.mwrap.box2d; + +namespace m = mcpplibs::mwrap::box2d::macros; +namespace b = mcpplibs::mwrap::box2d; +''' + +TEST_SAMPLES = ''' +namespace { + +// Null and non-null ids of every id type but b2WorldId (B2_ID_EQUALS does not +// apply to it upstream: it has no world0). +template +std::vector ids() { + std::vector out; + for (int index : {0, 1, 7, 0x7fffffff}) + for (int world : {0, 1, 3}) + for (int generation : {0, 1, 0xffff}) { + Id id{}; + id.index1 = index; + id.world0 = static_cast(world); + id.generation = static_cast(generation); + out.push_back(id); + } + return out; +} + +template +void unary(F check) { + for (auto v : ids()) check(v); + for (auto v : ids()) check(v); + for (auto v : ids()) check(v); + for (auto v : ids()) check(v); + for (auto v : ids()) check(v); + for (int index : {0, 1, 0xffff}) check(b2WorldId{static_cast(index), 1}); +} +template +void binary(F check) { + auto over = [&](const auto& values) { + for (auto a : values) + for (auto c : values) check(a, c); + }; + over(ids()); + over(ids()); + over(ids()); + over(ids()); +} + +// Restores the length unit a test changes. +struct LengthUnits { + explicit LengthUnits(float units) { b2SetLengthUnitsPerMeter(units); } + ~LengthUnits() { b2SetLengthUnitsPerMeter(1.0f); } +}; + +} // namespace +''' + + +def render_test(sf) -> str: + constants, runtime, functions = classify(sf) + out = [TEST_HEADER] + out.append('// ── constants ───────────────────────────────────────────────────────────────') + for n in constants: + out.append(f'constexpr auto raw_{n} = {n};') + for n in constants: + out += [f'#pragma push_macro("{n}")', f'#undef {n}', + f'static_assert(std::is_same_v && m::{n} == raw_{n});', + f'#pragma pop_macro("{n}")'] + out.append('\n// ── the static const objects ────────────────────────────────────────────────') + for c in sf.constants: + out.append(f'static_assert(std::is_same_v);') + out.append('') + out.append('TEST(StaticConstants, SameBytes) {') + for c in sf.constants: + out.append(f' EXPECT_EQ(std::memcmp(&b::{c.name}, &::{c.name}, sizeof(::{c.type})), 0) << "{c.name}";') + out.append('}') + out.append(TEST_SAMPLES) + out.append('// ── values that read b2_lengthUnitsPerMeter ─────────────────────────────────') + out.append('TEST(LengthUnitValues, FollowTheLengthUnit) {') + out.append(' auto check = [] {') + for n in runtime: + out += [f' auto raw_{n} = {n};', + f'#pragma push_macro("{n}")', f'#undef {n}', + f' static_assert(std::is_same_v);', + f' EXPECT_EQ(m::{n}(), raw_{n}) << "{n}";', + f'#pragma pop_macro("{n}")'] + out.append(' };') + out.append(' check();') + out.append(' LengthUnits pixels(32.0f);') + out.append(' check();') + out.append('}\n') + out.append('// ── computation macros ──────────────────────────────────────────────────────') + for macro in functions: + params = ', '.join(f'auto {p}' for p in macro.params) + args = ', '.join(macro.params) + arity = {1: 'unary', 2: 'binary'}[len(macro.params)] + out.append(f'TEST(MacroFunctions, {macro.name}) {{') + out.append(f' {arity}([]({params}) {{') + out += [f' static_assert(std::is_same_v);', + f' EXPECT_EQ({macro.name}({args}), (m::{macro.name})({args}));'] + out.append(' });') + out.append('}') + return '\n'.join(out) + '\n' + + +def outputs(root: Path) -> dict[Path, str]: + sf = surface(fetch(root)) + return { + root / 'src/box2d.cppm': render_root(sf), + root / 'src/macros.cppm': render_macros(sf), + root / 'tests/macros_mix_test.cpp': render_test(sf), + } + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--check', action='store_true') + args = parser.parse_args() + root = Path(__file__).resolve().parents[1] + stale = [] + for path, text in outputs(root).items(): + # Text mode: a checkout with CRLF line ends (Windows) reads the same. + current = path.read_text(encoding='utf-8') if path.exists() else None + if current == text: + continue # unchanged: the timestamp is kept, nothing rebuilds + if args.check: + stale.append(path.relative_to(root).as_posix()) + else: + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(text, encoding='utf-8', newline='\n') + print('wrote', path.relative_to(root).as_posix()) + if stale: + sys.exit('not what tools/gen_exports.py writes: ' + ', '.join(stale)) + + +if __name__ == '__main__': + main() diff --git a/tools/inventory.py b/tools/inventory.py new file mode 100644 index 0000000..cb1b138 --- /dev/null +++ b/tools/inventory.py @@ -0,0 +1,251 @@ +#!/usr/bin/env python3 +"""The public surface of Box2D 3.2.0, from the pinned release. + +Shared by tools/gen_exports.py (which writes the module sources and the +generated macro test) and tools/check_exports.py (which audits them). The +headers come from the upstream tag archive, downloaded once into +target/upstream and checked against upstream/documents.json: the same bytes +compat.box2d builds. + +The public headers are the ones upstream installs (src/CMakeLists.txt, +BOX2D_API_FILES). They are read as a C++ consumer of compat.box2d compiles +them: `__cplusplus` is defined, BOX2D_DOUBLE_PRECISION is not (single +precision, as the library is built), neither are BOX2D_USER_CONFIG, +BOX2D_EXPORT, BOX2D_DLL or box2d_EXPORTS (a static library). The remaining +conditionals (_MSC_VER, __GNUC__, NDEBUG, B2_ENABLE_ASSERT, BOX2D_VALIDATE) +only choose between two definitions of the same macro (B2_FORCE_INLINE, +B2_BREAKPOINT, B2_ASSERT, B2_VALIDATE, B2_ENABLE_VALIDATION): every branch is +read for its macro names, and a declaration under one of them is an error — +except under the assertion conditional, `!defined( NDEBUG ) || +defined( B2_ENABLE_ASSERT )`, which declares b2InternalAssert: the library +defines it under the same condition (src/core.c), so the module exports it +under that condition too (Surface.guarded). +""" +from dataclasses import dataclass, field +import hashlib +import json +from pathlib import Path +import re +import sys +import tarfile +import urllib.request + +INSTALLED = [ # src/CMakeLists.txt: BOX2D_API_FILES, in its order + 'base.h', 'box2d.h', 'collision.h', 'config.h', 'constants.h', 'id.h', 'math_functions.h', 'math_types.h', + 'types.h', +] + +# How this build answers each conditional that selects declarations. +DECIDED = { + '__cplusplus': True, + 'BOX2D_DOUBLE_PRECISION': False, + 'BOX2D_USER_CONFIG': False, + 'BOX2D_EXPORT': False, +} +# The assertion conditional: its declarations exist in a build without NDEBUG +# (or with B2_ENABLE_ASSERT), in the library and the consumer alike. +GUARD = '!defined( NDEBUG ) || defined( B2_ENABLE_ASSERT )' +# Conditionals that only choose between definitions of the same macros. +MACRO_ONLY = ['_MSC_VER', '__GNUC__', '__clang__', 'NDEBUG', 'B2_ENABLE_ASSERT', 'BOX2D_VALIDATE', + 'B2_ENABLE_VALIDATION', '_WIN32', 'BOX2D_DLL', 'box2d_EXPORTS', 'B2_MAX_WORLDS', 'B2_NAME_LENGTH'] + + +def fetch(root: Path) -> Path: + pins = json.loads((root / 'upstream/documents.json').read_text(encoding='utf-8')) + cache = root / 'target/upstream' + cache.mkdir(parents=True, exist_ok=True) + archive = cache / ('box2d-' + pins['upstream_version'] + '.tar.gz') + if not archive.exists() or hashlib.sha256(archive.read_bytes()).hexdigest() != pins['upstream_sha256']: + print('+ fetch', pins['upstream'], flush=True) + with urllib.request.urlopen(pins['upstream'], timeout=120) as response: + archive.write_bytes(response.read()) + digest = hashlib.sha256(archive.read_bytes()).hexdigest() + if digest != pins['upstream_sha256']: + sys.exit(f'upstream archive checksum {digest} != {pins["upstream_sha256"]}') + with tarfile.open(archive) as tar: + top = tar.getmembers()[0].name.split('/')[0] + if not (cache / top / 'include/box2d/box2d.h').exists(): + tar.extractall(cache, filter='data') + return cache / top + + +@dataclass +class Macro: + name: str + header: str + params: list[str] | None # None: object-like + body: str + + +@dataclass +class Constant: # `static const T name = init;` — internal linkage + name: str + type: str + init: str + + +@dataclass +class Surface: + types: list[str] = field(default_factory=list) # typedef / struct / enum names + opaque: list[str] = field(default_factory=list) # declared, not defined (b2Replay, b2Recording) + enumerators: dict[str, str] = field(default_factory=dict) # enumerator: its enum + functions: list[str] = field(default_factory=list) # B2_API + inline: list[str] = field(default_factory=list) # B2_INLINE / B2_FORCE_INLINE / B2_ID_INLINE + operators: list[str] = field(default_factory=list) # the C++ vector operators of math_functions.h + variables: list[str] = field(default_factory=list) # extern variables + constants: list[Constant] = field(default_factory=list) # static const objects + macros: dict[str, Macro] = field(default_factory=dict) + guarded: dict[str, str] = field(default_factory=dict) # declaration: the #if it is under + + +def strip(text: str) -> str: + text = re.sub(r'/\*.*?\*/', ' ', text, flags=re.S) + return re.sub(r'//[^\n]*', '', text) + + +def condition(expr: str) -> bool | None: + """True/False for a decided conditional, None for a macro-only one.""" + names = re.findall(r'\b[A-Za-z_]\w*\b', expr.replace('defined', ' ')) + if any(n in DECIDED for n in names): + if len(names) != 1: + sys.exit(f'a compound conditional over a decided name: {expr}') + value = DECIDED[names[0]] + return (not value) if expr.lstrip().startswith('!') or expr.startswith('ndef') else value + if all(n in MACRO_ONLY for n in names): + return None + sys.exit(f'a conditional the inventory does not know: {expr}') + + +def preprocess(text: str, header: str, s: Surface) -> str: + """The header's declarations as this build sees them; its macros into `s`.""" + out = [] + stack: list[tuple[bool | None | str, bool]] = [] # (active: True/False/None = macro-only/GUARD, taken) + lines = text.replace('\\\n', ' ').split('\n') + for line in lines: + d = re.match(r'^\s*#\s*(\w+)\s*(.*)$', line) + live = all(a is not False for a, _ in stack) + macro_only = any(a is None for a, _ in stack) + if d: + kind, rest = d.group(1), d.group(2).strip() + if kind == 'if' and rest == GUARD: + stack.append((GUARD, True)) + elif kind in ('if', 'ifdef', 'ifndef'): + expr = {'if': rest, 'ifdef': rest, 'ifndef': '!' + rest}[kind] + v = condition(expr) + stack.append((v, v is True)) + elif kind == 'elif': + v, taken = stack.pop() + c = condition(rest) + if v is None or c is None: + stack.append((None, True)) + else: + stack.append((c and not taken, taken or c)) + elif kind == 'else': + v, taken = stack.pop() + stack.append((None, True) if v is None or v == GUARD else (not taken, True)) + elif kind == 'endif': + stack.pop() + elif kind == 'define' and live: + m = re.match(r'(\w+)(\(([^)]*)\))?\s*(.*)$', rest) + params = [p.strip() for p in m.group(3).split(',')] if m.group(2) else None + s.macros.setdefault(m.group(1), Macro(m.group(1), header, params, ' '.join(m.group(4).split()))) + elif kind in ('pragma', 'include') or not live: + pass + else: + sys.exit(f'{header}: a directive the inventory does not know: {line}') + continue + guarded = any(a == GUARD for a, _ in stack) + if live and not macro_only and guarded: + out.append('@GUARD@ ' + line if line.strip() else line) + elif live and not macro_only: + out.append(line) + elif live and line.strip(): + sys.exit(f'{header}: a declaration under a macro-only conditional: {line.strip()}') + return '\n'.join(out) + + +def statements(code: str): + """Top-level statements: up to `;` at depth 0, or a function body's `}`.""" + start, depth, i = 0, 0, 0 + while i < len(code): + ch = code[i] + if ch == '{': + depth += 1 + elif ch == '}': + depth -= 1 + if depth == 0: + head = code[start:i] + head = head[:head.index('{')] + if re.search(r'\)\s*(const\s*)?$', head): + yield code[start:i + 1].strip() + start = i + 1 + elif ch == ';' and depth == 0: + yield code[start:i + 1].strip() + start = i + 1 + i += 1 + if code[start:].strip(): + sys.exit('unterminated declaration: ' + code[start:][:200]) + + +def surface(upstream: Path) -> Surface: + headers = upstream / 'include/box2d' + s = Surface() + for name in INSTALLED: + code = preprocess(strip((headers / name).read_text(encoding='utf-8')), name, s) + for st in statements(code): + one = ' '.join(st.split()) + if one.startswith('@GUARD@ '): + one = one.replace('@GUARD@ ', '') + g = re.search(r'(\w+) ?\(', one) + s.guarded[g.group(1)] = GUARD + if m := re.fullmatch(r'typedef (struct|enum) (\w+) \{(.*)\} (\w+) ?;', one): + if m.group(2) != m.group(4): + sys.exit(f'{name}: typedef of another name: {one[:120]}') + s.types.append(m.group(4)) + if m.group(1) == 'enum': + for e in re.findall(r'(?:^|,)\s*([A-Za-z_]\w*)\s*(?:=[^,]*)?', m.group(3)): + s.enumerators[e] = m.group(4) + elif m := re.fullmatch(r'typedef struct (\w+) (\w+) ?;', one): + if m.group(1) != m.group(2): + sys.exit(f'{name}: typedef of another name: {one}') + s.opaque.append(m.group(1)) + elif m := re.fullmatch(r'typedef [\w\s\*]+?\**\s*(\w+) ?\(.*\) ?;', one): + s.types.append(m.group(1)) # a function type: `typedef void b2TaskCallbackFcn( void* );` + elif m := re.fullmatch(r'typedef \w+ (\w+) ?;', one): + s.types.append(m.group(1)) + elif m := re.fullmatch(r'B2_API [^(]*?\b(\w+) ?\(.*\) ?;', one): + s.functions.append(m.group(1)) + elif m := re.fullmatch(r'(?:B2_INLINE|B2_FORCE_INLINE|B2_ID_INLINE) [^(]*?\b(\w+) ?\(.*\}', one): + s.inline.append(m.group(1)) + elif m := re.fullmatch(r'inline [^(]*?\b(operator\s*\S+?) ?\(.*\}', one): + s.operators.append(m.group(1).replace(' ', '')) + elif m := re.fullmatch(r'extern "C" BOX2D_EXPORT \w+ (\w+) ?;', one): + s.variables.append(m.group(1)) + elif m := re.fullmatch(r'static const (\w+) (\w+) = (.*) ?;', one): + s.constants.append(Constant(m.group(2), m.group(1), m.group(3))) + else: + sys.exit(f'{name}: a declaration the inventory does not know: {one[:200]}') + # Opaque forward declarations that are defined later (b2SimplexCache) are types. + s.opaque = sorted(set(s.opaque) - set(s.types)) + s.types = sorted(set(s.types)) + s.functions = sorted(set(s.functions)) + s.inline = sorted(set(s.inline)) + s.operators = sorted(set(s.operators)) + return s + + +if __name__ == '__main__': + root = Path(__file__).resolve().parents[1] + sf = surface(fetch(root)) + print(f'{len(sf.types)} types, {len(sf.opaque)} opaque, {len(sf.enumerators)} enumerators, ' + f'{len(sf.functions)} B2_API functions, {len(sf.inline)} inline functions, {len(sf.operators)} operators, ' + f'{len(sf.variables)} variables, {len(sf.constants)} static constants, {len(sf.macros)} macros') + print('types:', ' '.join(sf.types)) + print('opaque:', ' '.join(sf.opaque)) + print('enums:', ' '.join(sorted(set(sf.enumerators.values())))) + print('inline:', ' '.join(sf.inline)) + print('operators:', ' '.join(sf.operators)) + print('variables:', ' '.join(sf.variables)) + print('constants:', ' '.join(f'{c.type} {c.name} = {c.init}' for c in sf.constants)) + print('object-like:', ' '.join(f'{m.name}={m.body}' for m in sf.macros.values() if m.params is None)) + print('function-like:', ' '.join(m.name for m in sf.macros.values() if m.params is not None)) diff --git a/tools/run_determinism.py b/tools/run_determinism.py new file mode 100644 index 0000000..aa3e519 --- /dev/null +++ b/tools/run_determinism.py @@ -0,0 +1,85 @@ +#!/usr/bin/env python3 +"""Box2D's determinism on this machine, through examples/box2d. + +Builds examples/box2d (box2d-determinism, written against the module) in the +dev profile (-O0) and the release profile (optimized: GCC contracts a*b+c +into an FMA, where it is allowed to, only when optimizing), runs both +scenes in each under every configuration tests/determinism_test.cpp runs — 1, 2, +4 and 8 workers of the built-in scheduler, 4 workers of a user task system, +the SSE2 fallback with 1 and 4 workers — and requires every run of a scene to +give the same sleep step and hashes, and those to be the values the test +pins (upstream's for the falling hinges). It prints one line per profile and +scene, the line CI rows are compared by. + + python3 tools/run_determinism.py [--toolchain NAME@VERSION] [--target TRIPLE] +""" +import argparse +import json +from pathlib import Path +import platform +import re +import subprocess +import sys + +CONFIGS = [['--workers', '1'], ['--workers', '2'], ['--workers', '4'], ['--workers', '8'], + ['--workers', '4', '--tasks'], ['--workers', '1', '--sse2'], ['--workers', '4', '--sse2']] + + +def pins(root: Path) -> dict: + text = (root / 'tests/determinism_test.cpp').read_text(encoding='utf-8') + value = lambda name: int(re.search(r'\b' + name + r' = (-?(?:0x)?[0-9A-Fa-f]+)', text).group(1), 0) + return { + 'falling-hinges': {'sleepStep': value('kHingesSleepStep'), 'transforms': value('kHingesHash')}, + 'pyramid': {'sleepStep': value('kPyramidSleepStep'), 'transforms': value('kPyramidTransforms'), + 'events': value('kPyramidEvents'), 'state': value('kPyramidState')}, + } + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--toolchain') + parser.add_argument('--target') + parser.add_argument('--mcpp', default='mcpp') + args = parser.parse_args() + root = Path(__file__).resolve().parents[1] + example = root / 'examples/box2d' + select = (['--toolchain', args.toolchain] if args.toolchain else []) + (['--target', args.target] if args.target else []) + expected = pins(root) + failed = False + host = f'{platform.system()} {platform.machine()}' + (f' {args.target}' if args.target else '') + for profile in ('dev', 'release'): + chosen = [*select, '--profile', profile] + subprocess.run([args.mcpp, 'build', *chosen], cwd=example, check=True) + runs: dict[str, list[dict]] = {} + for config in CONFIGS: + out = subprocess.run([args.mcpp, 'run', *chosen, '--', '--json', *config], cwd=example, check=True, + capture_output=True, text=True).stdout + for line in out.splitlines(): + if line.startswith('{'): + run = json.loads(line) + runs.setdefault(run['scene'], []).append(run) + for scene, results in runs.items(): + keys = ('steps', 'sleepStep', 'transforms', 'events', 'state') + distinct = {tuple(r[k] for k in keys) for r in results} + first = results[0] + print(f'Box2D determinism, {host}{", " + args.toolchain if args.toolchain else ""}, {profile}: {scene}: ' + f'sleepStep={first["sleepStep"]} transforms={first["transforms"]} events={first["events"]} ' + f'state={first["state"]} - {len(results)} runs, {"identical" if len(distinct) == 1 else "DIFFERENT"}, ' + f'AVX2 {"available" if first["avx2"] else "not available"}', flush=True) + if len(distinct) != 1: + failed = True + for r in results: + print(' ', json.dumps(r)) + for key, want in expected[scene].items(): + got = first[key] if isinstance(first[key], int) else int(first[key], 16) + if got != want: + failed = True + print(f' {profile} {scene} {key}: 0x{got:X} != pinned 0x{want:X}') + if set(runs) != set(expected): + sys.exit(f'scenes {sorted(runs)} != {sorted(expected)}') + if failed: + sys.exit('Box2D is not deterministic here, or not as pinned') + + +if __name__ == '__main__': + main() diff --git a/tools/test_template.py b/tools/test_template.py new file mode 100644 index 0000000..81ea805 --- /dev/null +++ b/tools/test_template.py @@ -0,0 +1,83 @@ +#!/usr/bin/env python3 +"""Render an installed candidate's default mcpp template, then build and run it.""" +import argparse +import json +import os +from pathlib import Path +import shutil +import subprocess +import tempfile +import tomllib + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--mcpp", default="mcpp") + parser.add_argument("--namespace", help="index namespace for legacy manifests without package.namespace") + parser.add_argument("--toolchain", default="llvm@23.1.3") + parser.add_argument("--template", help="named template; omit for the package default") + parser.add_argument("--run-arg", action="append", default=[]) + parser.add_argument("--index", type=Path, help="candidate SDK/wrapper index for unpublished feature dependencies") + args = parser.parse_args() + root = Path(__file__).resolve().parents[1] + metadata = tomllib.loads((root / "mcpp.toml").read_text())["package"] + namespace = metadata.get("namespace", args.namespace) + assert namespace, "Supply the published index namespace for this legacy manifest" + name, version = metadata["name"], metadata["version"] + identity = namespace + "." + name + with tempfile.TemporaryDirectory(prefix=name + "-template-") as directory: + temporary = Path(directory) + home = temporary / "home" + package = home / "registry/data/xpkgs" / (namespace + "-x-" + name) / version / (name + "-" + version) + shutil.copytree(root, package, ignore=shutil.ignore_patterns( + ".git", "target", ".mcpp", ".xlings", ".candidate-index", "__pycache__", "mcpp.lock", "compile_commands.json", "mcpp-2026.*")) + index = home / "indices/mcpplibs/pkgs/mwrap" + index.mkdir(parents=True) + entry = '{url="https://example.invalid/candidate.tar.gz",sha256="fixture"}' + descriptor = ('package={spec="1",namespace=' + json.dumps(namespace) + ',name=' + json.dumps(name) + + ',description="Installed template fixture",type="package",mcpp="mcpp.toml",xpm={') + descriptor += ",".join(platform + "={[" + json.dumps(version) + "]=" + entry + "}" + for platform in ("linux", "macosx", "windows")) + "}}\n" + (index / (identity + ".lua")).write_text(descriptor) + env = dict(os.environ, MCPP_HOME=str(home)) + selector = identity + "@" + version + for command in ([args.mcpp, "new", "--list-templates", selector], + [args.mcpp, "new", "quickstart", "--template", selector + (":" + args.template if args.template else "")]): + print("+", " ".join(command), flush=True) + subprocess.run(command, cwd=temporary, env=env, check=True) + project = temporary / "quickstart" + manifest = project / "mcpp.toml" + text = manifest.read_text() + dependency = tomllib.loads(text)["dependencies"][identity] + options = {"version": dependency} if isinstance(dependency, str) else dependency + assert options["version"] == version, options + replacement = '{ path = ' + json.dumps(package.as_posix()) + if options.get("features"): + replacement += ', features = ' + json.dumps(options["features"]) + replacement += ' }' + lines = text.splitlines() + matches = [i for i, line in enumerate(lines) if line.startswith(json.dumps(identity) + " =")] + assert len(matches) == 1, "Expected exactly one qualified dependency from self.name" + lines[matches[0]] = json.dumps(identity) + " = " + replacement + manifest.write_text("\n".join(lines) + "\n") + if args.index: + candidate_index = args.index.resolve() + subprocess.run([os.sys.executable, str(candidate_index / "tests/prepare_mwrap_indices.py")], + cwd=candidate_index, check=True) + with manifest.open("a") as out: + out.write("\n[indices]\n") + for namespace in ("compat", "mcpplibs.mwrap"): + view = candidate_index / "tests/mwrap/.indices" / namespace + out.write(json.dumps(namespace) + ' = { path = ' + json.dumps(view.as_posix()) + ' }\n') + command = [args.mcpp, "build", "--toolchain", args.toolchain, "--strict"] + print("+", " ".join(command), flush=True) + subprocess.run(command, cwd=project, check=True) + command = [args.mcpp, "run", "--toolchain", args.toolchain] + if args.run_arg: + command += ["--", *args.run_arg] + print("+", " ".join(command), flush=True) + subprocess.run(command, cwd=project, check=True) + + +if __name__ == "__main__": + main() diff --git a/upstream/box2d/LICENSE b/upstream/box2d/LICENSE new file mode 100644 index 0000000..e32198e --- /dev/null +++ b/upstream/box2d/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 Erin Catto + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/upstream/box2d/README.md b/upstream/box2d/README.md new file mode 100644 index 0000000..b60c760 --- /dev/null +++ b/upstream/box2d/README.md @@ -0,0 +1,239 @@ +# Box2D + +Box2D is a 2D physics engine for games. + +![Box2D Logo](https://box2d.org/images/logo.svg) + +[![Box2D Version 3.0 Release +Demo](https://img.youtube.com/vi/dAoM-xjOWtA/0.jpg)](https://www.youtube.com/watch?v=dAoM-xjOWtA) + +## Build Status + +[![Build +Status](https://github.com/erincatto/box2d/actions/workflows/build.yml/badge.svg)](https://github.com/erincatto/box2d/actions) + +## Features + +### Collision + +- Continuous collision detection +- Contact events +- Convex polygons, capsules, circles, rounded polygons, segments, and chains +- Multiple shapes per body +- Collision filtering +- Ray casts, shape casts, and overlap queries +- Sensor system + +### Physics + +- Robust _Soft Step_ rigid body solver +- Continuous physics for fast translations and rotations +- Island based sleep +- Revolute, prismatic, distance, motor, weld, and wheel joints +- Joint limits, motors, springs, and friction +- Joint and contact forces +- Body movement events and sleep notification + +### Character movement + +- Geometric mover with robust plane solver +- Dynamic mover with custom joint support + +### System + +- Data-oriented design +- Written in portable C17 +- Extensive multithreading and SIMD +- Optimized for large piles of bodies +- Low cost double-precision support for large worlds +- Recording and replay +- Cross platform determinism +- Snapshot and restore + +### Samples + +- OpenGL with GLFW +- Graphical user interface with imgui +- Many samples to demonstrate features and performance + +## Building All Platforms + +- Install [CMake](https://cmake.org/) +- Install [git](https://git-scm.com/) +- Ensure these run from the command line + +## Building with CMake presets + +The presets in `CMakePresets.json` give one build flow on every platform and are picked up +automatically by Visual Studio, VS Code, and CLion (open the folder and choose a preset). From the +command line: + +- Windows: `cmake --preset windows` then `cmake --build --preset windows-release` +- Linux: `cmake --preset linux-release` then `cmake --build --preset linux-release` +- macOS: `cmake --preset macos` then `cmake --build --preset macos-release` + +Use the `*-debug` build presets for a debug build (not recommended for the replay viewer). The +presets use the default native toolchain (the installed Visual Studio on Windows, Make on Linux, +Xcode on macOS), so no specific compiler version is required. + +## Building for Visual Studio + +- Install [Visual Studio](https://visualstudio.microsoft.com/) +- Run `build_vs2026.bat` for Visual Studio 2026, or use the `windows` preset above for other + versions +- Open and build the generated solution in the `build` folder + +## Building for Linux + +- Run `build.sh` from a bash shell +- Results are in the build sub-folder + +## Building for Xcode + +- mkdir build +- cd build +- cmake -G Xcode .. +- Open `box2d.xcodeproj` +- Select the samples scheme +- Build and run the samples + +## Building and installing + +- mkdir build +- cd build +- cmake .. +- cmake --build . --config Release +- cmake --install . (might need sudo) + +Installing also provides a pkg-config file, so `pkg-config --modversion box2d` reports the installed +version and `pkg-config --cflags --libs box2d` gives the build flags. + +## Building with zig + +Fetch and link Box2D from a Zig project: + +- `zig fetch --save git+https://github.com/erincatto/box2d` + +In `build.zig`: + +```zig +const box2d_dep = b.dependency("box2d", .{}); +exe.root_module.addImport("box2d", box2d_dep.module("box2d")); +``` + +In Zig code, start using box2d + +```zig +const box2d = @import("box2d"); + +pub fn main() void { + var world_def = box2d.b2DefaultWorldDef(); + world_def.gravity.y = -10.0; + const world_id = box2d.b2CreateWorld(&world_def); + defer box2d.b2DestroyWorld(world_id); + + box2d.b2World_Step(world_id, 1.0 / 60.0, 4); +} +``` + +## Building with Swift Package Manager + +Add box2d as a dependency in your `Package.swift`: + +```swift +.package(url: "https://github.com/erincatto/box2d.git", branch: "main") +``` + +And add the product to your target: + +```swift +.target( + name: "MyGame", + dependencies: [.product(name: "box2d", package: "box2d")] +), +``` + +In Swift code, the C API is imported directly: + +```swift +import box2d + +var worldDef = b2DefaultWorldDef() +let worldId = b2CreateWorld(&worldDef) +``` + +C structs are passed as `inout` arguments using `&`. + +## Replay viewer + +The samples app doubles as a viewer for Box2D recordings (`.b2rec` files). Any preset above builds +it. Pass a recording on the command line to open it directly: + +- Windows: `build\bin\Release\samples.exe path\to\session.b2rec` +- Linux: `build/bin/samples path/to/session.b2rec` +- macOS: `build/bin/Release/samples path/to/session.b2rec` + +On Windows you can also drag a `.b2rec` file onto `samples.exe`. The viewer runs from any directory. +Without an argument, open a recording from the **Replay** menu. See +[docs/recording.md](docs/recording.md) for how to make a recording. + +## Compatibility + +The Box2D library and samples build and run on Windows, Linux, and Mac. + +You will need a compiler that supports C17 to build the Box2D library. + +You will need a compiler that supports C++20 to build the samples. + +Box2D uses SSE2, AVX2, and Neon (AArch64) SIMD math to improve performance. This can be disabled by +defining `BOX2D_DISABLE_SIMD`. SSE2 is automatically used as a fallback when the CPU doesn't support +AVX2. + +## Documentation + +- [Manual](https://box2d.org/documentation/) + +## Community + +- [Discord](https://discord.gg/NKYgCBP) + +## Contributing + +Please do not submit pull requests. Instead, please file an issue for bugs or feature requests. For +support, please visit the Discord server. + +## Giving feedback + +Please file an issue or start a chat on discord. You can also use [GitHub +Discussions](https://github.com/erincatto/box2d/discussions). + +## License + +Box2D is developed by Erin Catto and uses the [MIT +license](https://en.wikipedia.org/wiki/MIT_License). + +## Sponsorship + +Support development of Box2D through [Github Sponsors](https://github.com/sponsors/erincatto). + +Please consider starring this repository and subscribing to my [YouTube +channel](https://www.youtube.com/@erin_catto). + +## LLM Usage + +LLMs are used in the following areas: + +- unit tests +- samples app +- migrating code between Box2D and Box3D +- build configuration +- code reviews +- benchmarking + +Elsewhere all code is developed and written by me. + +## External ports, wrappers, and bindings (unsupported) + +- Beef bindings - https://github.com/EnokViking/Box2DBeef +- C++ bindings - https://github.com/HolyBlackCat/box2cpp +- WASM - https://github.com/Birch-san/box2d3-wasm diff --git a/upstream/documents.json b/upstream/documents.json new file mode 100644 index 0000000..b4082d7 --- /dev/null +++ b/upstream/documents.json @@ -0,0 +1,18 @@ +{ + "upstream": "https://github.com/erincatto/box2d/archive/refs/tags/v3.2.0.tar.gz", + "upstream_version": "3.2.0", + "upstream_sha256": "ffec747b27a2267016f8d0240d658c82d187fb2271fa0f69fcbe53fbc067180d", + "pruning": "Nothing of Box2D is vendored: the library is compat.box2d 3.2.0, built from the same tag archive (every src/*.c, -ffp-contract=off, upstream's default SIMD selection). Its README and MIT license are retained here.", + "documents": [ + { + "path": "upstream/box2d/README.md", + "source": "https://github.com/erincatto/box2d/blob/v3.2.0/README.md", + "sha256": "49bee2e2c091343e0fcaba9dbff3d243c8496fc4d7f8f25849a7f499ca41011a" + }, + { + "path": "upstream/box2d/LICENSE", + "source": "https://github.com/erincatto/box2d/blob/v3.2.0/LICENSE", + "sha256": "68a3e676d7e94093b102d5cba0d4e04af812040d6f230c3db67a6664574e43d2" + } + ] +}