diff --git a/.agents/docs/2026-11-15-add-jolt-mwrap-plan.md b/.agents/docs/2026-11-15-add-jolt-mwrap-plan.md new file mode 100644 index 00000000..7b7df5c0 --- /dev/null +++ b/.agents/docs/2026-11-15-add-jolt-mwrap-plan.md @@ -0,0 +1,45 @@ +# Add mcpplibs.mwrap.jolt (jolt-m 5.6.0) + +## Form + +- Third-party upstream [jrouwe/JoltPhysics](https://github.com/jrouwe/JoltPhysics), release v5.6.0, MIT. +- Form A external mwrap module repository [mcpplibs/jolt-m](https://github.com/mcpplibs/jolt-m), package + `mcpplibs.mwrap.jolt`, version 5.6.0 (the first indexed release of the identity), tag `mwrap-5.6.0`; release asset + `jolt-m-5.6.0.tar.gz` (`ea4b7052…9c49`), CN mirror `gitcode.com/mcpp-res/mwrap-jolt` with identical bytes. +- Implementation ownership: the library has no compat package in this index, so the wrapper vendors `Jolt/` from the + release archive (checksums of every file in `upstream/documents.json`) and builds it as upstream's CMake build does + with `CROSS_PLATFORM_DETERMINISTIC` on (`JPH_CROSS_PLATFORM_DETERMINISTIC`, `-ffp-contract=off`, no fast math), + with `JPH_NO_DEBUG` (the same library and module names in every profile: upstream turns on its asserts and + debug-only members when `NDEBUG` is not defined), and without the parts upstream builds only in its Debug and + Release configurations or as options: no debug renderer, profiler, object stream or GPU compute backend (DX12, + Vulkan, Metal and their CPU emulation are not vendored). The SIMD level is the target's baseline. It deploys + upstream's README and LICENSE with its own LICENSE/NOTICE (`runtime.deploy`). On wasm32 it links with a 1 MiB stack + and growing memory. + +## Module + +- `mcpplibs.mwrap.jolt`: every name declared at namespace scope in `JPH` and its nested namespaces by the headers under + `` (the debug renderer's, the compute shaders' shared C++/HLSL headers and a list of macro invocations + excepted), by `using` in the namespace upstream declares it in — `JPH` (450 types, 127 functions and operators, 6 + objects), `ClosestPoint`, `HalfFloatConversion`, `ScaleHelpers`, `StreamUtils`, `literals`. +- Internal-linkage adaptation: 50 `static` or non-`inline` `constexpr` objects (`cMaxPhysicsJobs`, + `cMaxPhysicsBarriers`, `cDefaultConvexRadius`, `JPH_PI`, the half-float layout, the height field's constants …) are + declared again under their names, with their types and values, in `mcpplibs::mwrap::jolt` and its nested namespaces + of the same names; a constant array as a `std::array` of its elements. Two `static` narrow-phase helpers + (`ActiveEdges::FixNormal`, `IsEdgeActive`) are not exported. +- A name declared on some targets only is exported under upstream's own condition: `JPH::FPControlWord` (SSE and Arm, + not wasm32). +- No `.macros` module: Jolt's macros are configuration switches and code generation (`JPH_DECLARE_RTTI_*`, + `JPH_OVERRIDE_NEW_DELETE`, `JPH_ASSERT`). +- The export list is derived by `tools/surface.py` from the headers as clang reads them (its JSON AST), recorded in + `tools/surface.json`, and audited in CI: the module is the record on every row, and on the Linux LLVM row the record + is derived again and compared. + +## Verification + +- Wrapper CI (Linux x86_64 and arm64 with GCC 16.1 and LLVM 23.1.3, macOS LLVM, Windows LLVM, each in the dev and + release profiles; wasm32-emscripten under node): a stack knocked down by a ball, a ray and a point query, a world + saved and restored running on to the uninterrupted run's bits, a scene's hash after 200 steps pinned and the same + on every row with 0, 1 and 4 workers; the adapted constants; header mixing; the export audit; the default template + from an installed copy with a strict build. +- This index: consumer `tests/mwrap/jolt-world`; the installed-template check with `--deploy` (`STATIC_NOTICES`). diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 3152391e..424c10a5 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -1733,6 +1733,11 @@ jobs: shell: bash run: python3 tests/mwrap/check_installed_template.py --mcpp "$MCPP" --package mcpplibs.mwrap.manifold@3.5.5.1 --deploy + - name: Installed jolt wrapper archive, actual default world template and its relocated notices + if: contains(format(' {0} ', env.MEMBERS), ' jolt-world ') && matrix.toolchain == 'default' + shell: bash + run: python3 tests/mwrap/check_installed_template.py --mcpp "$MCPP" --package mcpplibs.mwrap.jolt@5.6.0 --deploy + - name: Installed libjpeg-turbo wrapper archive, actual default decoding template and its relocated notices if: contains(format(' {0} ', env.MEMBERS), ' libjpeg-turbo-decode ') && matrix.toolchain == 'default' shell: bash diff --git a/CHANGELOG.md b/CHANGELOG.md index 9c57a84d..c4abe674 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -41,6 +41,15 @@ ### Added +- **mcpplibs.mwrap.jolt 5.6.0(新包)。** jolt-m:Jolt Physics 5.6.0(刚体与软体物理:形状、约束、查询、角色、载具)的 C++23 + 模块,以 `export using` 在上游的命名空间里导出 `` 下各头文件的全部公开名字(`JPH` 450 个类型、127 个函数与运算符、 + 6 个对象,及嵌套命名空间 `ClosestPoint`、`HalfFloatConversion`、`ScaleHelpers`、`StreamUtils`、`literals` 的名字);50 个内部链接的 + 常量(`cMaxPhysicsJobs`、`cDefaultConvexRadius`、`JPH_PI` 等;常量数组为 `std::array`)以原名位于 `mcpplibs::mwrap::jolt`。导出清单 + 由 clang 读头文件推出并在 CI 中审计;`JPH::FPControlWord` 只在上游声明它的平台(SSE、Arm)导出。封装仓库从上游 5.6.0 发布包取 + 源码并以跨平台确定模式构建(`JPH_CROSS_PLATFORM_DETERMINISTIC`、`-ffp-contract=off`、不用快速数学,`JPH_NO_DEBUG` 使各配置相同): + 同一场景在 Linux x86-64 与 arm64、macOS、Windows、wasm32 上,dev 与 release,0、1、4 个工作线程,哈希相同(钉住)。不含调试 + 渲染器、性能剖析、对象流与 GPU 计算后端。消费者测试 `tests/mwrap/jolt-world`,安装后模板检查带 `--deploy`。GLOBAL 为 GitHub + 发布资产,CN 为 GitCode `mcpp-res/mwrap-jolt` 镜像,sha256 相同。 - **mcpplibs.mwrap.basis_universal 2.50.1(新包)。** basis-universal-m:Basis Universal 2.50 转码器(把 KTX2 与 `.basis` 纹理转成 像素或 GPU 自己的压缩格式)的 C++23 模块,`` 在 `basist`、`basisu` 及其内部命名空间声明的 603 个名字(148 个类型、273 个函数、70 个对象、112 个无作用域枚举的枚举项)以原名导出;头文件里 139 个内部链接的标量常量 diff --git a/mcpp.toml b/mcpp.toml index cd5bea10..8719a961 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -38,6 +38,7 @@ members = [ "tests/mwrap/meshoptimizer-simplify", "tests/mwrap/cgltf-inspect", "tests/mwrap/manifold-boolean", + "tests/mwrap/jolt-world", "tests/mwrap/basis-universal-transcode", "tests/mwrap/libjpeg-turbo-decode", "tests/mwrap/libarchive-bundle", diff --git a/pkgs/mwrap/jolt.lua b/pkgs/mwrap/jolt.lua new file mode 100644 index 00000000..442a8a18 --- /dev/null +++ b/pkgs/mwrap/jolt.lua @@ -0,0 +1,50 @@ +-- Immutable upstream-aligned mwrap source release (jolt-m mwrap-5.6.0, the first release of upstream 5.6.0); +-- GLOBAL and CN bytes are identical. +-- +-- The wrapper is Apache-2.0; the library it vendors and links statically, +-- Jolt Physics 5.6.0, is MIT: the package deploys its README and LICENSE +-- beside programs (runtime.deploy). +package = { + spec = "1", + namespace = "mcpplibs.mwrap", + name = "jolt", + description = "jolt-m: Jolt Physics 5.6.0 (rigid and soft body physics: shapes, constraints, queries, characters, vehicles), cross-platform deterministic, as a C++23 module", + licenses = { "Apache-2.0" }, + repo = "https://github.com/mcpplibs/jolt-m", + type = "package", + extensions = { + autoupdate = { + tag = "mwrap-${version}", + url = "https://github.com/mcpplibs/jolt-m/releases/download/${tag}/jolt-m-${version}.tar.gz", + }, + }, + xpm = { + linux = { + ["5.6.0"] = { + url = { + GLOBAL = "https://github.com/mcpplibs/jolt-m/releases/download/mwrap-5.6.0/jolt-m-5.6.0.tar.gz", + CN = "https://gitcode.com/mcpp-res/mwrap-jolt/releases/download/5.6.0/jolt-m-5.6.0.tar.gz", + }, + sha256 = "ea4b7052c13888142594a01966081bda80a2dc8a32e9e093479cfacaba939c49", + }, + }, + macosx = { + ["5.6.0"] = { + url = { + GLOBAL = "https://github.com/mcpplibs/jolt-m/releases/download/mwrap-5.6.0/jolt-m-5.6.0.tar.gz", + CN = "https://gitcode.com/mcpp-res/mwrap-jolt/releases/download/5.6.0/jolt-m-5.6.0.tar.gz", + }, + sha256 = "ea4b7052c13888142594a01966081bda80a2dc8a32e9e093479cfacaba939c49", + }, + }, + windows = { + ["5.6.0"] = { + url = { + GLOBAL = "https://github.com/mcpplibs/jolt-m/releases/download/mwrap-5.6.0/jolt-m-5.6.0.tar.gz", + CN = "https://gitcode.com/mcpp-res/mwrap-jolt/releases/download/5.6.0/jolt-m-5.6.0.tar.gz", + }, + sha256 = "ea4b7052c13888142594a01966081bda80a2dc8a32e9e093479cfacaba939c49", + }, + }, + }, +} diff --git a/tests/mwrap/check_installed_template.py b/tests/mwrap/check_installed_template.py index 604f79a1..242749eb 100644 --- a/tests/mwrap/check_installed_template.py +++ b/tests/mwrap/check_installed_template.py @@ -23,6 +23,7 @@ "mcpplibs.mwrap.meshoptimizer": ("meshoptimizer.cppm", "upstream/meshoptimizer/LICENSE.md", b"MIT License", b"Arseny Kapoulkine"), "mcpplibs.mwrap.cgltf": ("cgltf.cppm", "upstream/cgltf/LICENSE", b"Permission is hereby granted", b"Kuhlmann"), "mcpplibs.mwrap.manifold": ("manifold.cppm", "upstream/manifold/LICENSE", b"Apache License", b"The Manifold Authors"), + "mcpplibs.mwrap.jolt": ("jolt.cppm", "upstream/jolt/LICENSE", b"Permission is hereby granted", b"Jolt Physics is Copyright (c) 2021 Jorrit"), "mcpplibs.mwrap.libjpeg_turbo": ("libjpeg_turbo.cppm", "upstream/libjpeg-turbo/README.ijg", b"Independent JPEG Group", b"libjpeg-turbo Project"), "mcpplibs.mwrap.basis_universal": ("basis_universal.cppm", "upstream/basis_universal/NOTICE", b"Binomial LLC", b"Basis Universal"), "mcpplibs.mwrap.libc": ("libc.cppm", None, b"", b"ISO/IEC 9899:2018"), diff --git a/tests/mwrap/jolt-world/mcpp.toml b/tests/mwrap/jolt-world/mcpp.toml new file mode 100644 index 00000000..1bf785cf --- /dev/null +++ b/tests/mwrap/jolt-world/mcpp.toml @@ -0,0 +1,15 @@ +[package] +name = "jolt-world" +version = "5.6.0" +description = "Consume the immutable Jolt Physics wrapper: a world of bodies stepped, queried, saved and restored to the same bits, with one worker and several" + +[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.jolt = "5.6.0" + +# Jolt's inline math compiled here too, as the library is built. +[build] +cxxflags = ["-ffp-contract=off"] diff --git a/tests/mwrap/jolt-world/tests/behavior.cpp b/tests/mwrap/jolt-world/tests/behavior.cpp new file mode 100644 index 00000000..c3bb55f4 --- /dev/null +++ b/tests/mwrap/jolt-world/tests/behavior.cpp @@ -0,0 +1,109 @@ +// The installed Jolt Physics wrapper: a floor, a stack of boxes and a ball thrown at it; stepped until the stack +// falls, a ray cast down onto it, the world saved and restored to the uninterrupted run's bits, and the same bits +// with one worker and with three; the adapted constants. +import std; +import mcpplibs.mwrap.jolt; + +namespace jolt = mcpplibs::mwrap::jolt; + +namespace { + +constexpr JPH::ObjectLayer still = 0, moving = 1; + +bool map(JPH::BroadPhaseLayerInterfaceTable& broad, JPH::ObjectLayerPairFilterTable& pairs) { + broad.MapObjectToBroadPhaseLayer(still, JPH::BroadPhaseLayer(0)); + broad.MapObjectToBroadPhaseLayer(moving, JPH::BroadPhaseLayer(1)); + pairs.EnableCollision(moving, still); + pairs.EnableCollision(moving, moving); + return true; +} +struct Layers { + JPH::BroadPhaseLayerInterfaceTable broad{2, 2}; + JPH::ObjectLayerPairFilterTable pairs{2}; + bool mapped = map(broad, pairs); + JPH::ObjectVsBroadPhaseLayerFilterTable object_vs_broad{broad, 2, pairs, 2}; +}; + +struct Scene { + JPH::TempAllocatorImpl temp{4 * 1024 * 1024}; + std::unique_ptr jobs; + JPH::PhysicsSystem system; + std::vector bodies; +}; + +void make(Scene& scene, const Layers& layers, int workers) { + if (workers == 0) scene.jobs = std::make_unique(jolt::cMaxPhysicsJobs); + else scene.jobs = std::make_unique(jolt::cMaxPhysicsJobs, jolt::cMaxPhysicsBarriers, workers); + scene.system.Init(256, 0, 256, 256, layers.broad, layers.object_vs_broad, layers.pairs); + JPH::BodyInterface& bodies = scene.system.GetBodyInterface(); + scene.bodies.push_back(bodies.CreateAndAddBody( + JPH::BodyCreationSettings(new JPH::BoxShape(JPH::Vec3(20, 1, 20)), JPH::RVec3(0, -1, 0), JPH::Quat::sIdentity(), JPH::EMotionType::Static, still), + JPH::EActivation::DontActivate)); + for (int level = 0; level < 5; ++level) + scene.bodies.push_back(bodies.CreateAndAddBody(JPH::BodyCreationSettings(new JPH::BoxShape(JPH::Vec3(0.5f, 0.5f, 0.5f)), JPH::RVec3(0, 0.5f + level * 1.0f, 0), + JPH::Quat::sIdentity(), JPH::EMotionType::Dynamic, moving), + JPH::EActivation::Activate)); + JPH::BodyCreationSettings ball(new JPH::SphereShape(0.5f), JPH::RVec3(-8, 2, 0), JPH::Quat::sIdentity(), JPH::EMotionType::Dynamic, moving); + ball.mLinearVelocity = JPH::Vec3(20, 1, 0); + scene.bodies.push_back(bodies.CreateAndAddBody(ball, JPH::EActivation::Activate)); +} + +void step(Scene& scene, int steps) { + for (int index = 0; index < steps; ++index) scene.system.Update(1.0f / 60.0f, 1, &scene.temp, scene.jobs.get()); +} + +std::uint64_t bits(const Scene& scene) { + std::uint64_t hash = 0xcbf29ce484222325ull; + const JPH::BodyInterface& bodies = scene.system.GetBodyInterface(); + for (const JPH::BodyID body : scene.bodies) { + const JPH::RVec3 at = bodies.GetPosition(body); + const JPH::Quat turn = bodies.GetRotation(body); + for (const float value : {float(at.GetX()), float(at.GetY()), float(at.GetZ()), turn.GetX(), turn.GetY(), turn.GetZ(), turn.GetW()}) { + hash ^= std::bit_cast(value); + hash *= 0x100000001b3ull; + } + } + return hash; +} + +} // namespace + +int main() { + JPH::RegisterDefaultAllocator(); + JPH::Factory::sInstance = new JPH::Factory(); + JPH::RegisterTypes(); + int failed = 0; + { + const Layers layers; + Scene straight; + make(straight, layers, 0); + JPH::RayCastResult hit; + if (!straight.system.GetNarrowPhaseQuery().CastRay(JPH::RRayCast{JPH::RVec3(0, 20, 0), JPH::Vec3(0, -40, 0)}, hit) || hit.mBodyID != straight.bodies[5]) failed = 1; + step(straight, 180); + const std::uint64_t expected = bits(straight); + if (straight.system.GetBodyInterface().GetPosition(straight.bodies[5]).GetY() > 3.0f && !failed) failed = 2; // the top box fell + + Scene rolled; + make(rolled, layers, 0); + step(rolled, 60); + JPH::StateRecorderImpl saved; + rolled.system.SaveState(saved); + step(rolled, 60); + saved.Rewind(); + if (!rolled.system.RestoreState(saved) && !failed) failed = 3; + step(rolled, 120); + if (bits(rolled) != expected && !failed) failed = 4; + + Scene threaded; + make(threaded, layers, 3); + step(threaded, 180); + if (bits(threaded) != expected && !failed) failed = 5; + if ((jolt::cMaxPhysicsJobs != 2048 || jolt::sAllSubShapeTypes.size() != static_cast(jolt::NumSubShapeTypes)) && !failed) failed = 6; + std::println("Jolt Physics 5.6.0 through import mcpplibs.mwrap.jolt: the stack knocked down, restored and run again to {:#018x}, the same with 3 workers{}", + expected, failed ? std::format(" — check {} failed", failed) : ""); + } + JPH::UnregisterTypes(); + delete JPH::Factory::sInstance; + JPH::Factory::sInstance = nullptr; + return failed; +}