From 613f67364b871ca244ef643cccf5e5fd45aa9fa9 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Wed, 28 Oct 2026 19:03:19 +0900 Subject: [PATCH] mcpplibs.mwrap.box2d 3.2.0: box2d-m (Box2D as C++23 modules) Descriptor pkgs/mwrap/box2d.lua (form A, the immutable mwrap-3.2.0 release asset, tagged on box2d-m's merged main; CN is the gitcode mcpp-res/mwrap-box2d mirror with the same bytes), over compat.box2d 3.2.0. Consumer tests/mwrap/box2d-physics: a box that falls, rests and sleeps, a ray cast, the mapped constants and id macros, and the wrapper's struck 20-row pyramid, whose transform hash (0x790D7C28) is the one every box2d-m CI row computes, in a consumer compiled without any floating-point flag; and the side header beside the import. The installed-template check in validate.yml with its deployment contract (the wrapper's LICENSE/NOTICE, Box2D's README and LICENSE, byte for byte in a relocated bundle), and the plan. --- .../docs/2026-10-28-add-box2d-mwrap-plan.md | 61 ++++++++++++ .github/workflows/validate.yml | 5 + CHANGELOG.md | 10 ++ mcpp.toml | 1 + pkgs/mwrap/box2d.lua | 50 ++++++++++ tests/mwrap/box2d-physics/mcpp.toml | 11 +++ tests/mwrap/box2d-physics/tests/behavior.cpp | 93 +++++++++++++++++++ .../mwrap/box2d-physics/tests/side_header.cpp | 20 ++++ tests/mwrap/check_installed_template.py | 20 ++++ 9 files changed, 271 insertions(+) create mode 100644 .agents/docs/2026-10-28-add-box2d-mwrap-plan.md create mode 100644 pkgs/mwrap/box2d.lua create mode 100644 tests/mwrap/box2d-physics/mcpp.toml create mode 100644 tests/mwrap/box2d-physics/tests/behavior.cpp create mode 100644 tests/mwrap/box2d-physics/tests/side_header.cpp diff --git a/.agents/docs/2026-10-28-add-box2d-mwrap-plan.md b/.agents/docs/2026-10-28-add-box2d-mwrap-plan.md new file mode 100644 index 00000000..c5028ac6 --- /dev/null +++ b/.agents/docs/2026-10-28-add-box2d-mwrap-plan.md @@ -0,0 +1,61 @@ +# Add mcpplibs.mwrap.box2d (box2d-m 3.2.0) + +## Form + +- Third-party upstream [erincatto/box2d](https://github.com/erincatto/box2d), release v3.2.0 (2026-10-19), MIT. + The tag archive (`ffec747b…180d`) is the one compat.box2d 3.2.0 pins (the parent change). +- Form A external mwrap module repository [mcpplibs/box2d-m](https://github.com/mcpplibs/box2d-m), package + `mcpplibs.mwrap.box2d`, version 3.2.0 (first release of the identity, no revision suffix), tag series + `mwrap-${version}` on the merged main commit (`361aa31`); release asset `box2d-m-3.2.0.tar.gz` (`a7b448ea…7572`), + CN mirror `gitcode.com/mcpp-res/mwrap-box2d` with identical bytes. +- Implementation ownership: the library is compat.box2d's (a static library of every src/*.c, single precision, + `-ffp-contract=off`, upstream's default SIMD selection; its configuration and ABI are sufficient). The wrapper vendors + no upstream code. It retains upstream's README.md and LICENSE under `upstream/box2d/`, with checksums in + `upstream/documents.json`, and deploys them with its own LICENSE/NOTICE (`runtime.deploy`). + +## Modules + +- `mcpplibs.mwrap.box2d`: by `export using`, generated from the pinned installed headers (`BOX2D_API_FILES`): 110 + types, 3 opaque structs, 176 enumerators, 506 `B2_API` functions (`b2InternalAssert` under the header's own + `!defined(NDEBUG) || defined(B2_ENABLE_ASSERT)`), 91 inline functions (`B2_INLINE` / `B2_FORCE_INLINE` / + `B2_ID_INLINE` are `inline` in C++: external linkage, exportable), 8 C++ vector operators and + `b2_lengthUnitsPerMeter`. The 13 `static const` objects (internal linkage: `b2_nullBodyId`, `b2Rot_identity`, + `b2Vec2_zero` …) are `inline constexpr` of the same type and initializer in `mcpplibs::mwrap::box2d`. +- `mcpplibs.mwrap.box2d.macros`: 20 object-like macros as `inline constexpr` of the type of their expansion; the 5 that + read `b2_lengthUnitsPerMeter` (`B2_HUGE`, `B2_LINEAR_SLOP`, `B2_SPECULATIVE_DISTANCE`, `B2_CONTACT_RECYCLE_DISTANCE`, + `B2_MAX_AABB_MARGIN`) as functions of no arguments, since `b2SetLengthUnitsPerMeter` changes them at run time; and + `B2_IS_NULL`, `B2_IS_NON_NULL`, `B2_ID_EQUALS` as constexpr function templates whose body is the macro. +- Stay macros, in `` (= ``): `BOX2D_EXPORT`, `B2_API`, `B2_INLINE`, + `B2_FORCE_INLINE`, `B2_ID_INLINE`, `B2_ALIGN_AS`, `B2_LITERAL`, `B2_ZERO_INIT`, `B2_NULL_ID`, `B2_ASSERT`, + `B2_VALIDATE`, `B2_BREAKPOINT`. + +`tools/check_exports.py` fails when the generated sources are stale, a declaration is not exported, a static constant +not adapted, or a macro neither mapped nor listed; every mapping is checked against the raw header in a header-mixing +test. + +## Determinism + +Wrapper CI (Linux x86_64 GCC 16.1 and LLVM 23.1.3, Linux arm64 GCC 16.1 and LLVM 23.1.3, macOS LLVM, Windows LLVM, +and wasm32-emscripten under node; all required) runs, in the dev and the release profile, upstream's falling-hinges +scene and a 20-row pyramid struck by a ball (600 steps, sleep off) under 1, 2, 4 and 8 workers of the built-in +scheduler, 4 workers of a user task system and the SSE2 fallback. Every row gives the same values: falling hinges +asleep at step 299, transform hash `0x465F3DF4` (upstream's pin), event hash `0xD019CCFF`, state hash +`0x8BD019D8675ACE67`; pyramid transform hash `0x790D7C28`, event hash `0xE8B80415`, state hash `0xE4E1D89FFCE4C43E`. +Built with `-mfma -ffp-contract=fast` instead, an optimized x86_64 build gives 256 / `0xD513C6E1` and `0x1F3DD87B`: +the flag is what the hashes hold. + +## Verification + +Here, with the CI-pinned mcpp 2026.10.26.1: + +- `bash tests/run_members.sh box2d-physics`: the real release archive through the candidate index; a box that falls, + rests and sleeps, a ray cast, the macros and constants, and the struck pyramid's hash `0x790D7C28` in a consumer + compiled without any floating-point flag; and the side header beside the import: `test result ok`. +- `python3 tests/mwrap/check_installed_template.py --package mcpplibs.mwrap.box2d@3.2.0 --deploy`: the unmodified + default `physics` template from the real archive, then a relocated self-contained bundle whose `licenses/box2d/` + holds the wrapper's LICENSE/NOTICE and Box2D's README and LICENSE byte for byte. + +## Motivation + +openmui's game physics (mgame.physics) needs a deterministic 2D engine whose results are the same on every row of +its CI, the Web included, with pinned hashes; Box2D v3 is that engine, and this module is how openmui imports it. diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index e27dadee..a6d65157 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -1638,6 +1638,11 @@ jobs: shell: bash run: python3 tests/mwrap/check_installed_template.py --mcpp "$MCPP" --package mcpplibs.mwrap.sheenbidi@3.0.0 --deploy + - name: Installed Box2D wrapper archive, actual default physics template and its relocated notices + if: contains(format(' {0} ', env.MEMBERS), ' box2d-physics ') && matrix.toolchain == 'default' + shell: bash + run: python3 tests/mwrap/check_installed_template.py --mcpp "$MCPP" --package mcpplibs.mwrap.box2d@3.2.0 --deploy + - name: Installed miniaudio wrapper archive, actual default tone template and its relocated notices if: contains(format(' {0} ', env.MEMBERS), ' miniaudio-engine ') && matrix.toolchain == 'default' shell: bash diff --git a/CHANGELOG.md b/CHANGELOG.md index aadd443b..0d5cd325 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,16 @@ ### Added +- **mcpplibs.mwrap.box2d 3.2.0(新包)。** box2d-m:以 C++23 模块(`import mcpplibs.mwrap.box2d;`)使用 + compat.box2d 3.2.0。按上游全局名导出已安装头文件的 110 个类型、3 个不透明结构体、176 个枚举值、506 个 `B2_API` 函数、 + 91 个内联函数、8 个 C++ 向量运算符与 `b2_lengthUnitsPerMeter`;头文件的 13 个 `static const` 对象(内部链接)以同类型、 + 同初值的常量置于 `mcpplibs::mwrap::box2d`。`mcpplibs.mwrap.box2d.macros` 把 20 个宏映射为常量、5 个读取长度单位的宏 + 映射为函数(`B2_LINEAR_SLOP()`)、3 个 id 宏映射为 constexpr 函数;声明说明符、初始化器语法与断言留在 + ``。包装仓库的 CI(Linux x86_64 GCC/LLVM、Linux arm64 GCC/LLVM、macOS、Windows、 + wasm32-emscripten)在 dev 与 release 下运行确定性场景:上游的 falling hinges(第 299 步、哈希 0x465F3DF4)与 + 被撞倒的 20 层金字塔(600 步,哈希 0x790D7C28),1–8 个 worker、用户任务系统、SSE2 回退,各行逐位一致。 + GLOBAL 为 `mwrap-3.2.0` 发布资产,CN 为 GitCode `mcpp-res/mwrap-box2d` 镜像,sha256 一致。新增测试成员 + `tests/mwrap/box2d-physics` 与已安装模板及其许可文件的部署检查。 - **compat.box2d 3.2.0(新包)。** Box2D v3:游戏用 2D 刚体物理引擎(C17,MIT,无依赖)。按上游 CMake 的做法从标签归档 构建 `src/*.c` 为静态库:`-std=gnu17`、`-ffp-contract=off`(上游为跨平台确定性对每个 GCC/Clang 设置,禁止乘加收缩为 FMA)、上游默认的 SIMD 选择(x86_64 为 SSE2,并在运行时按 cpuid 分派 AVX2 求解器;arm64 为 Neon)、unix 上 diff --git a/mcpp.toml b/mcpp.toml index 559afd7c..dcf78520 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -23,6 +23,7 @@ members = [ "tests/mwrap/concurrentqueue-mpmc", "tests/mwrap/fribidi-bidi", "tests/mwrap/sheenbidi-bidi", + "tests/mwrap/box2d-physics", "tests/mwrap/miniaudio-engine", "tests/mwrap/freetype-render", "tests/mwrap/harfbuzz-shape", diff --git a/pkgs/mwrap/box2d.lua b/pkgs/mwrap/box2d.lua new file mode 100644 index 00000000..86cd9023 --- /dev/null +++ b/pkgs/mwrap/box2d.lua @@ -0,0 +1,50 @@ +-- Immutable upstream-aligned mwrap source release (box2d-m mwrap-3.2.0); +-- GLOBAL and CN bytes are identical. +-- +-- The wrapper is Apache-2.0; the library it links, compat.box2d (Box2D 3.2.0, +-- single precision, -ffp-contract=off), is MIT, linked statically: the package +-- deploys Box2D's README and LICENSE beside programs (runtime.deploy). +package = { + spec = "1", + namespace = "mcpplibs.mwrap", + name = "box2d", + description = "box2d-m: Box2D 3.2.0 (the 2D rigid-body physics engine, cross-platform deterministic) as C++23 modules", + licenses = { "Apache-2.0" }, + repo = "https://github.com/mcpplibs/box2d-m", + type = "package", + extensions = { + autoupdate = { + tag = "mwrap-${version}", + url = "https://github.com/mcpplibs/box2d-m/releases/download/${tag}/box2d-m-${version}.tar.gz", + }, + }, + xpm = { + linux = { + ["3.2.0"] = { + url = { + GLOBAL = "https://github.com/mcpplibs/box2d-m/releases/download/mwrap-3.2.0/box2d-m-3.2.0.tar.gz", + CN = "https://gitcode.com/mcpp-res/mwrap-box2d/releases/download/3.2.0/box2d-m-3.2.0.tar.gz", + }, + sha256 = "a7b448ea45e29edfb36059f0a6c6acd1f24655deae61742f64287a71bf197572", + }, + }, + macosx = { + ["3.2.0"] = { + url = { + GLOBAL = "https://github.com/mcpplibs/box2d-m/releases/download/mwrap-3.2.0/box2d-m-3.2.0.tar.gz", + CN = "https://gitcode.com/mcpp-res/mwrap-box2d/releases/download/3.2.0/box2d-m-3.2.0.tar.gz", + }, + sha256 = "a7b448ea45e29edfb36059f0a6c6acd1f24655deae61742f64287a71bf197572", + }, + }, + windows = { + ["3.2.0"] = { + url = { + GLOBAL = "https://github.com/mcpplibs/box2d-m/releases/download/mwrap-3.2.0/box2d-m-3.2.0.tar.gz", + CN = "https://gitcode.com/mcpp-res/mwrap-box2d/releases/download/3.2.0/box2d-m-3.2.0.tar.gz", + }, + sha256 = "a7b448ea45e29edfb36059f0a6c6acd1f24655deae61742f64287a71bf197572", + }, + }, + }, +} diff --git a/tests/mwrap/box2d-physics/mcpp.toml b/tests/mwrap/box2d-physics/mcpp.toml new file mode 100644 index 00000000..e05e7479 --- /dev/null +++ b/tests/mwrap/box2d-physics/mcpp.toml @@ -0,0 +1,11 @@ +[package] +name = "box2d-physics" +version = "3.2.0" +description = "Consume the immutable Box2D wrapper: a world that steps, collides and sleeps, a ray cast, the mapped constants and id macros, the struck-pyramid determinism hash every platform computes, the side header beside the import" + +[indices] +# Use the complete candidate built-in index prepared by tests/run_members.sh. +# An explicit empty table overrides the legacy workspace redirect. + +[dependencies] +"mcpplibs.mwrap.box2d" = "3.2.0" diff --git a/tests/mwrap/box2d-physics/tests/behavior.cpp b/tests/mwrap/box2d-physics/tests/behavior.cpp new file mode 100644 index 00000000..4f34a4f6 --- /dev/null +++ b/tests/mwrap/box2d-physics/tests/behavior.cpp @@ -0,0 +1,93 @@ +// The installed wrapper, as a consumer uses it: only the module, no header. +// A box falls onto the ground and sleeps; a ray finds it; the mapped +// constants and id macros; and the wrapper's struck-pyramid scene, whose +// final-state hash is the one every platform computes (box2d-m pins it on +// Linux x86_64 and arm64, macOS, Windows and wasm32). Its setup is exact in +// binary, so this consumer needs no floating-point flag of its own. +import std; +import mcpplibs.mwrap.box2d; + +using namespace mcpplibs::mwrap::box2d; // b2_nullBodyId, b2Vec2_zero … +namespace m = mcpplibs::mwrap::box2d::macros; + +namespace { + +b2BodyId ground(b2WorldId world) { + b2BodyDef def = b2DefaultBodyDef(); + def.position = b2Vec2{0.0f, -1.0f}; + b2BodyId body = b2CreateBody(world, &def); + b2Polygon box = b2MakeBox(40.0f, 1.0f); + b2ShapeDef shape = b2DefaultShapeDef(); + b2CreatePolygonShape(body, &shape, &box); + return body; +} + +// box2d-m's examples/box2d/src/scenes.hpp, pyramid(): 210 unit boxes, a +// ball at 24 m/s, sleep off, 600 steps. +std::uint32_t pyramid() { + b2WorldDef worldDef = b2DefaultWorldDef(); + worldDef.enableSleep = false; + b2WorldId world = b2CreateWorld(&worldDef); + ground(world); + b2Polygon box = b2MakeBox(0.5f, 0.5f); + b2ShapeDef shapeDef = b2DefaultShapeDef(); + std::vector bodies; + for (int i = 0; i < 20; ++i) + for (int j = 0; j < 20 - i; ++j) { + b2BodyDef def = b2DefaultBodyDef(); + def.type = b2_dynamicBody; + def.position = b2Vec2{static_cast(j) + 0.5f * static_cast(i) - 9.5f, static_cast(i) + 0.5f}; + bodies.push_back(b2CreateBody(world, &def)); + b2CreatePolygonShape(bodies.back(), &shapeDef, &box); + } + b2BodyDef ballDef = b2DefaultBodyDef(); + ballDef.type = b2_dynamicBody; + ballDef.position = b2Vec2{-30.0f, 3.0f}; + ballDef.linearVelocity = b2Vec2{24.0f, 4.0f}; + ballDef.isBullet = true; + bodies.push_back(b2CreateBody(world, &ballDef)); + b2Circle circle = {b2Vec2_zero, 1.0f}; + b2ShapeDef ballShape = b2DefaultShapeDef(); + ballShape.density = 8.0f; + b2CreateCircleShape(bodies.back(), &ballShape, &circle); + for (int step = 0; step < 600; ++step) b2World_Step(world, 1.0f / 60.0f, 4); + 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)); + } + b2DestroyWorld(world); + return hash; +} + +} // namespace + +int main() { + b2Version v = b2GetVersion(); + if (v.major != m::B2_VERSION_MAJOR || v.minor != 2 || v.revision != 0) return 1; + + b2WorldDef worldDef = b2DefaultWorldDef(); + b2WorldId world = b2CreateWorld(&worldDef); + ground(world); + b2BodyDef def = b2DefaultBodyDef(); + def.type = b2_dynamicBody; + def.position = b2Vec2{0.0f, 4.0f}; + b2BodyId body = b2CreateBody(world, &def); + b2Polygon square = b2MakeBox(0.5f, 0.5f); + b2ShapeDef shape = b2DefaultShapeDef(); + b2CreatePolygonShape(body, &shape, &square); + for (int i = 0; i < 240; ++i) b2World_Step(world, 1.0f / 60.0f, 4); + if (std::abs(b2Body_GetPosition(body).y - 0.5f) > 0.01f) return 2; + if (b2Body_IsAwake(body)) return 3; + b2RayResult hit = b2World_CastRayClosest(world, b2Vec2{0.0f, 10.0f}, b2Vec2{0.0f, -20.0f}, b2DefaultQueryFilter()); + if (!hit.hit || !m::B2_ID_EQUALS(b2Shape_GetBody(hit.shapeId), body)) return 4; + if (!m::B2_IS_NULL(b2_nullBodyId) || m::B2_IS_NULL(body)) return 5; + b2DestroyWorld(world); + + static_assert(m::B2_MAX_POLYGON_VERTICES == 8 && m::B2_PI == 3.14159265359f); + if (m::B2_LINEAR_SLOP() != 0.005f) return 6; + + const std::uint32_t hash = pyramid(); + std::println("struck pyramid, 600 steps: transform hash 0x{:08X}", hash); + return hash == 0x790D7C28u ? 0 : 7; +} diff --git a/tests/mwrap/box2d-physics/tests/side_header.cpp b/tests/mwrap/box2d-physics/tests/side_header.cpp new file mode 100644 index 00000000..2a9e583e --- /dev/null +++ b/tests/mwrap/box2d-physics/tests/side_header.cpp @@ -0,0 +1,20 @@ +// The side header from the installed package, beside the import: the raw +// header's macros and static constants with the module's entities. +#include + +import std; +import mcpplibs.mwrap.box2d; + +B2_API int mwrap_box2d_consumer(int value); +int mwrap_box2d_consumer(int value) { return value * 2; } + +int main() { + namespace m = mcpplibs::mwrap::box2d::macros; + namespace b = mcpplibs::mwrap::box2d; + b2BodyId none = B2_NULL_ID; + if (!B2_IS_NULL(none) || B2_IS_NULL(none) != (m::B2_IS_NULL)(none)) return 1; + if (std::memcmp(&::b2Rot_identity, &b::b2Rot_identity, sizeof(b2Rot)) != 0) return 2; + if (&::b2World_Step != &b2World_Step) return 3; + static_assert(B2_MAX_POLYGON_VERTICES == 8 && std::is_same_v); + return mwrap_box2d_consumer(21) == 42 ? 0 : 4; +} diff --git a/tests/mwrap/check_installed_template.py b/tests/mwrap/check_installed_template.py index 69d8011e..eb9d57c3 100644 --- a/tests/mwrap/check_installed_template.py +++ b/tests/mwrap/check_installed_template.py @@ -183,6 +183,26 @@ def main(): notice = (roots[0] / "NOTICE").read_bytes() assert b"mcpp-community" in notice and b"Muhammad Tayyab Akram" in notice print(f"Verified {len(declared)} byte-identical wrapper/SheenBidi license files", flush=True) + elif identity == "mcpplibs.mwrap.box2d": + # Box2D is MIT and linked statically: its LICENSE and README + # travel with the program beside the wrapper's LICENSE/NOTICE. + sources = [path for store in (home / "xpkgs", home / "registry/data/xpkgs") + for path in store.glob(f"mcpplibs.mwrap-x-box2d/{version}/*/src/box2d.cppm")] + assert len(sources) == 1, sources + source = sources[0].parent.parent + declared = tomllib.loads((source / "mcpp.toml").read_text(encoding="utf-8"))["runtime"]["deploy"] + roots = list(relocated.rglob("licenses/box2d")) + assert len(roots) == 1, f"Missing Box2D runtime license tree: {roots}" + for entry in declared: + notice = source / entry["from"] + copied = roots[0].parent.parent / entry["to"] / notice.name + assert copied.is_file() and copied.read_bytes() == notice.read_bytes(), copied + assert b"MIT License" in (roots[0] / "upstream/box2d/LICENSE").read_bytes() + assert b"Erin Catto" in (roots[0] / "upstream/box2d/LICENSE").read_bytes() + assert b"Apache License" in (roots[0] / "LICENSE").read_bytes() + notice = (roots[0] / "NOTICE").read_bytes() + assert b"mcpp-community" in notice and b"Erin Catto" in notice + print(f"Verified {len(declared)} byte-identical wrapper/Box2D license files", flush=True) elif identity == "mcpplibs.mwrap.miniaudio": # miniaudio (MIT No Attribution or the Unlicense) is linked # statically: its LICENSE and README travel with the program