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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,14 @@

### Added

- **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 上
`-fPIC -fvisibility=hidden`,Linux 链接 `-lm -lpthread`;Emscripten 目标加上游同样的 `-msimd128 -msse2`(`target_cfg`)。
不定义 BOX2D_VALIDATE / BOX2D_DOUBLE_PRECISION / BOX2D_PROFILE(与上游作为依赖使用时相同)。使用者 `#include <box2d/box2d.h>`。
GLOBAL 为 GitHub 标签归档,CN 为 GitCode `mcpp-res` 镜像,sha256 两处一致;声明 `extensions.autoupdate`(`v${version}` 标签)。
新增测试成员 `tests/examples/box2d`(三平台):版本、下落与休眠、接触与移动事件、射线与凸包,以及上游的确定性场景
falling hinges(第 299 步全部休眠、变换哈希 0x465F3DF4,1 个与 4 个 worker、SSE2 回退路径)。
- **mcpplibs.mwrap.miniaudio 0.11.25(新包)。** miniaudio-m:以 C++23 模块(`import mcpplibs.mwrap.miniaudio;`)使用
compat.miniaudio 0.11.25(MIT-0 或 Unlicense)。导出 `miniaudio.h` API 部分的全部 266 个类型与结构体标签、368 个枚举值与
931 个 `MA_API` 函数(上游全局名不变),每个声明位于头文件声明它的同一预处理条件下;三个 `static MA_INLINE` 辅助函数在
Expand Down
1 change: 1 addition & 0 deletions mcpp.toml
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ members = [
"tests/examples/safeint-c-api",
"tests/examples/sdl2",
"tests/examples/sdl3",
"tests/examples/box2d",
"tests/examples/sheenbidi",
"tests/examples/spdlog",
"tests/examples/fontconfig",
Expand Down
140 changes: 140 additions & 0 deletions pkgs/c/compat.box2d.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
-- compat.box2d — Box2D v3, the 2D rigid-body physics engine for games, MIT.
--
-- Box2D 3.x is a C17 rewrite of the engine (bodies, shapes, joints, contact
-- and sensor events, ray and shape casts, continuous collision, a built-in
-- task scheduler) with no dependencies beyond the C library. Upstream
-- documents it as cross-platform deterministic: the same inputs give the
-- same bits on x86_64 (SSE2 or AVX2) and arm64 (Neon), with any worker
-- count, as long as the floating point is built as upstream builds it. Its
-- own test suite pins that as a hash (test/test_determinism.c, the "falling
-- hinges" scene: sleep step 299, hash 0x465F3DF4 in single precision).
--
-- ─────────────────────────────────────────────────────────────────────────
-- THE BUILD UPSTREAM'S CMAKE MAKES, AND WHY EACH FLAG
--
-- sources every src/*.c (src/CMakeLists.txt's BOX2D_SOURCE_FILES is
-- exactly the .c files of src/; the .inl files are included
-- by contact_solver_w4.c / _w8.c and recording.c).
-- C17, GNU `C_STANDARD 17`, `C_EXTENSIONS YES`: c_standard "gnu17"
-- (-std=gnu17, the last -std on the line).
-- -ffp-contract=off
-- CMakeLists.txt "Deterministic math": contraction of a*b+c
-- into an FMA is the one value-changing transform compilers
-- enable by default, and whether it fires depends on the
-- target (arm64 always has FMA; x86_64 only under AVX2/FMA,
-- which the AVX2 solver file is). Upstream sets it for every
-- GCC and Clang, and for clang-cl as /clang:-ffp-contract=off;
-- this index's Windows toolchain is clang's GNU-style driver
-- targeting MSVC, which takes the plain flag. Without it the
-- determinism hash differs between rows.
-- SIMD upstream's default (BOX2D_DISABLE_SIMD off): src/core.h
-- picks SSE2 on x86_64 and Neon on arm64 from the compiler's
-- predefined macros, and on x86_64 additionally compiles
-- contact_solver_w8.c under `#pragma GCC target("avx2")` /
-- `clang attribute push(target("avx2"))`, chosen at run time
-- by cpuid (src/simd.c) — no -mavx2 for the build. The 4-wide
-- and 8-wide solvers give the same bits (upstream's
-- SSE2-fallback determinism test).
-- -fvisibility=hidden
-- `CMAKE_C_VISIBILITY_PRESET hidden`: of a shared object a
-- consumer links this into, only its own exports are visible;
-- the B2_API declarations need no attribute in a static
-- library (BOX2D_EXPORT is empty without box2d_EXPORTS).
-- -fPIC unix: linked into shared objects by its consumers. Not on
-- Windows (PE/COFF; clang rejects it for the MSVC target).
-- -lm -lpthread linux: libm for sqrtf/atan2f, pthreads for the built-in
-- scheduler's workers (src/timer.c). macOS has both in
-- libSystem; Windows uses CreateThread (kernel32).
--
-- NOT DEFINED, as a FetchContent / installed consumer of upstream gets it:
-- BOX2D_VALIDATE (a top-level-only option; only effective without NDEBUG),
-- B2_ENABLE_ASSERT (RelWithDebInfo only), BOX2D_PROFILE (Tracy),
-- BOX2D_DOUBLE_PRECISION (large worlds; it is PUBLIC upstream because it
-- changes the headers' types, so it would be a separate package, not a
-- consumer define). B2_ASSERT is on when NDEBUG is not defined, upstream's
-- rule (include/box2d/base.h).
--
-- kind = "lib": a static library; consumers `#include <box2d/box2d.h>`.
-- src/ is not on the consumer's include path: the sources include their
-- internal headers in quote form beside themselves.
package = {
spec = "1",
namespace = "compat",
name = "box2d",
description = "Box2D 3.2.0 — the 2D rigid-body physics engine for games (C17, cross-platform deterministic), MIT",
licenses = {"MIT"},
repo = "https://github.com/erincatto/box2d",
type = "package",

extensions = {
autoupdate = {
tag = "v${version}",
url = "https://github.com/erincatto/box2d/archive/refs/tags/${tag}.tar.gz",
},
},

-- One source tarball for every platform.
xpm = {
linux = {
["3.2.0"] = {
url = {
GLOBAL = "https://github.com/erincatto/box2d/archive/refs/tags/v3.2.0.tar.gz",
CN = "https://gitcode.com/mcpp-res/box2d/releases/download/3.2.0/box2d-3.2.0.tar.gz",
},
sha256 = "ffec747b27a2267016f8d0240d658c82d187fb2271fa0f69fcbe53fbc067180d",
},
},
macosx = {
["3.2.0"] = {
url = {
GLOBAL = "https://github.com/erincatto/box2d/archive/refs/tags/v3.2.0.tar.gz",
CN = "https://gitcode.com/mcpp-res/box2d/releases/download/3.2.0/box2d-3.2.0.tar.gz",
},
sha256 = "ffec747b27a2267016f8d0240d658c82d187fb2271fa0f69fcbe53fbc067180d",
},
},
windows = {
["3.2.0"] = {
url = {
GLOBAL = "https://github.com/erincatto/box2d/archive/refs/tags/v3.2.0.tar.gz",
CN = "https://gitcode.com/mcpp-res/box2d/releases/download/3.2.0/box2d-3.2.0.tar.gz",
},
sha256 = "ffec747b27a2267016f8d0240d658c82d187fb2271fa0f69fcbe53fbc067180d",
},
},
},

mcpp = {
language = "c++23",
import_std = false,
c_standard = "gnu17",

-- The public headers, included as <box2d/box2d.h>. They declare the
-- API as `extern "C"` under C++ themselves (B2_API).
include_dirs = { "*/include" },

sources = { "*/src/*.c" },
cflags = { "-ffp-contract=off" },

-- The Web: src/core.h maps wasm onto the SSE2 path, whose intrinsics
-- compile only with wasm SIMD enabled (upstream's CMakeLists.txt sets
-- exactly these two for EMSCRIPTEN). A program running it needs a
-- browser or node with wasm SIMD (every current one).
target_cfg = {
["cfg(os = \"emscripten\")"] = {
cflags = { "-msimd128", "-msse2" },
},
},

linux = {
cflags = { "-fPIC", "-fvisibility=hidden" },
ldflags = { "-lm", "-lpthread" },
},
macosx = {
cflags = { "-fPIC", "-fvisibility=hidden" },
},

targets = { box2d = { kind = "lib" } },
deps = {},
},
}
16 changes: 16 additions & 0 deletions tests/examples/box2d/mcpp.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# compat.box2d — the 2D physics engine (MIT), on every platform the descriptor
# declares: a world that steps, collides, casts and sleeps, and upstream's
# cross-platform determinism scene with the hash upstream pins.
[package]
name = "box2d-tests"
version = "0.1.0"
standard = "c++23"

# The test's own scene setup (positions such as h + 2*h*i) is floating point
# too: compiled as upstream compiles its tests, without contraction into FMA,
# or the arm64 rows would start from different bits.
[build]
cxxflags = ["-ffp-contract=off"]

[dependencies]
compat.box2d = "3.2.0"
208 changes: 208 additions & 0 deletions tests/examples/box2d/tests/box2d.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
// compat.box2d — Box2D 3.2.0, on a world that needs it.
//
// WHAT THIS ASSERTS AND WHY
//
// the version b2GetVersion, the linked library's own numbers
// a falling box gravity, a contact with the ground, a body at rest
// on it, and its sleep — the solver end to end
// events contact begin events, body move events
// queries a ray cast and an AABB overlap against the world
// geometry b2MakeBox's mass, b2ComputeHull of a point cloud
// determinism upstream's "falling hinges" scene (shared/
// determinism.c, test/test_determinism.c): 80 boxes
// in hinged pairs fall and settle; the step on which
// the world falls asleep and the djb2 hash of every
// body's final transform are upstream's pinned values
// (299, 0x465F3DF4) — with one worker, with four
// workers of the built-in scheduler, and on the 4-wide
// SSE2 solver path where the CPU would take AVX2.
//
// A pass means the library was built as upstream builds it: the hash moves
// with any value-changing floating-point transform (FMA contraction above all),
// on any row.

#include <box2d/box2d.h>
#include <box2d/math_functions.h>

#include <cstdint>
#include <cstdio>
#include <vector>

namespace {

int failures = 0;

void check(bool ok, const char *what)
{
std::printf("%-62s %s\n", what, ok ? "ok" : "FAILED");
if (!ok) ++failures;
}

struct Hinges {
std::vector<b2BodyId> bodies;
int sleepStep = -1;
std::uint32_t hash = 0;
};

// shared/determinism.c, CreateFallingHinges, in C++.
Hinges createFallingHinges(b2WorldId world)
{
{
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;
Hinges data;
const float h = 0.25f;
b2Polygon box = b2MakeSquare(h);
b2ShapeDef shapeDef = b2DefaultShapeDef();

b2RevoluteJointDef jointDef = b2DefaultRevoluteJointDef();
jointDef.enableLimit = true;
jointDef.lowerAngle = -0.1f * B2_PI;
jointDef.upperAngle = 0.2f * 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;

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 = b2_nullBodyId;
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 = b2_nullBodyId;
}
b2CreatePolygonShape(body, &shapeDef, &box);
data.bodies.push_back(body);
}
}
return data;
}

// Steps until every body sleeps; upstream's UpdateFallingHinges.
Hinges runFallingHinges(int workerCount, bool sse2Fallback)
{
b2WorldDef worldDef = b2DefaultWorldDef();
worldDef.workerCount = workerCount;
b2WorldId world = b2CreateWorld(&worldDef);
b2World_EnableSSE2Fallback(world, sse2Fallback);
Hinges data = createFallingHinges(world);
for (int step = 0; step < 1000 && data.hash == 0; ++step) {
b2World_Step(world, 1.0f / 60.0f, 4);
if (b2World_GetBodyEvents(world).moveCount == 0) {
data.hash = B2_HASH_INIT;
for (b2BodyId body : data.bodies) {
b2WorldTransform t = b2Body_GetTransform(body);
data.hash = b2Hash(data.hash, reinterpret_cast<const std::uint8_t *>(&t), sizeof t);
}
data.sleepStep = step;
}
}
b2DestroyWorld(world);
return data;
}

} // namespace

int main()
{
b2Version v = b2GetVersion();
check(v.major == 3 && v.minor == 2 && v.revision == 0, "b2GetVersion is 3.2.0");
check(!b2IsDoublePrecision(), "single precision (BOX2D_DOUBLE_PRECISION is off)");
std::printf("AVX2 available: %s\n", b2IsAVX2Available() ? "yes" : "no");

// A box falls onto the ground, touches it, comes to rest and sleeps.
{
b2WorldDef worldDef = b2DefaultWorldDef();
b2WorldId world = b2CreateWorld(&worldDef);
check(b2World_IsValid(world), "b2CreateWorld");

b2BodyDef groundDef = b2DefaultBodyDef();
b2BodyId ground = b2CreateBody(world, &groundDef);
b2Segment segment = {{-20.0f, 0.0f}, {20.0f, 0.0f}};
b2ShapeDef groundShape = b2DefaultShapeDef();
b2CreateSegmentShape(ground, &groundShape, &segment);

b2BodyDef bodyDef = b2DefaultBodyDef();
bodyDef.type = b2_dynamicBody;
bodyDef.position = b2Pos{0.0f, 4.0f};
b2BodyId body = b2CreateBody(world, &bodyDef);
b2Polygon box = b2MakeBox(0.5f, 0.5f);
b2ShapeDef shapeDef = b2DefaultShapeDef();
shapeDef.density = 1.0f;
shapeDef.enableContactEvents = true;
b2CreatePolygonShape(body, &shapeDef, &box);

b2MassData mass = b2Body_GetMassData(body);
check(mass.mass > 0.999f && mass.mass < 1.001f, "a 1x1 box of density 1 weighs 1");

int begins = 0, moves = 0;
for (int i = 0; i < 240; ++i) {
b2World_Step(world, 1.0f / 60.0f, 4);
begins += b2World_GetContactEvents(world).beginCount;
moves += b2World_GetBodyEvents(world).moveCount;
}
b2Pos p = b2Body_GetPosition(body);
check(p.y > 0.49f && p.y < 0.51f, "the box rests on the ground (y = 0.5)");
check(begins >= 1, "a contact begin event");
check(moves > 0, "body move events");
check(!b2Body_IsAwake(body), "the box falls asleep");

b2RayResult hit = b2World_CastRayClosest(world, b2Pos{0.0f, 10.0f}, b2Vec2{0.0f, -20.0f}, b2DefaultQueryFilter());
check(hit.hit && B2_ID_EQUALS(b2Shape_GetBody(hit.shapeId), body), "a ray cast down hits the box first");
check(hit.point.y > 0.99f && hit.point.y < 1.01f, "at its top face (y = 1)");

b2DestroyWorld(world);
check(!b2World_IsValid(world), "b2DestroyWorld");
}

// Geometry: the convex hull of a square with an interior point.
{
b2Vec2 points[] = {{0, 0}, {1, 0}, {1, 1}, {0, 1}, {0.5f, 0.5f}};
b2Hull hull = b2ComputeHull(points, 5);
check(hull.count == 4 && b2ValidateHull(&hull), "b2ComputeHull drops the interior point");
b2Polygon polygon = b2MakePolygon(&hull, 0.0f);
b2MassData mass = b2ComputePolygonMass(&polygon, 2.0f);
check(mass.mass > 1.999f && mass.mass < 2.001f, "a unit square of density 2 weighs 2");
}

// Upstream's determinism scene and hash.
const struct { int workers; bool sse2; const char *what; } runs[] = {
{1, false, "falling hinges, 1 worker: sleep step 299, hash 0x465F3DF4"},
{4, false, "falling hinges, 4 workers: sleep step 299, hash 0x465F3DF4"},
{1, true, "falling hinges, SSE2 fallback: sleep step 299, hash 0x465F3DF4"},
};
for (const auto &run : runs) {
Hinges data = runFallingHinges(run.workers, run.sse2);
std::printf(" workers=%d sse2Fallback=%d sleepStep=%d hash=0x%08X\n", run.workers, run.sse2 ? 1 : 0,
data.sleepStep, data.hash);
check(data.sleepStep == 299 && data.hash == 0x465F3DF4u, run.what);
}

std::printf("%s\n", failures == 0 ? "all checks passed" : "FAILURES");
return failures == 0 ? 0 : 1;
}
Loading