From 0fec6cc8f85b6aad15552f77be6079a767d593e6 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Sun, 15 Nov 2026 02:39:27 +0900 Subject: [PATCH] mcpplibs.mwrap.acl 2.1.0: acl-m (the Animation Compression Library and Realtime Math, skeletal clips compressed within a stated error, as a C++23 module) --- .agents/docs/2026-11-15-add-acl-mwrap-plan.md | 42 +++++++++ .github/workflows/validate.yml | 5 ++ CHANGELOG.md | 8 ++ mcpp.toml | 1 + pkgs/mwrap/acl.lua | 51 +++++++++++ tests/mwrap/acl-clip/mcpp.toml | 15 ++++ tests/mwrap/acl-clip/tests/behavior.cpp | 90 +++++++++++++++++++ tests/mwrap/check_installed_template.py | 1 + 8 files changed, 213 insertions(+) create mode 100644 .agents/docs/2026-11-15-add-acl-mwrap-plan.md create mode 100644 pkgs/mwrap/acl.lua create mode 100644 tests/mwrap/acl-clip/mcpp.toml create mode 100644 tests/mwrap/acl-clip/tests/behavior.cpp diff --git a/.agents/docs/2026-11-15-add-acl-mwrap-plan.md b/.agents/docs/2026-11-15-add-acl-mwrap-plan.md new file mode 100644 index 00000000..59ac12db --- /dev/null +++ b/.agents/docs/2026-11-15-add-acl-mwrap-plan.md @@ -0,0 +1,42 @@ +# Add mcpplibs.mwrap.acl (acl-m 2.1.0) + +## Form + +- Third-party upstreams [nfrechette/acl](https://github.com/nfrechette/acl), release v2.1.0, MIT, and the + [nfrechette/rtm](https://github.com/nfrechette/rtm) v2.2.0 it pins (its submodule at that tag), MIT. Both are + header-only. +- Form A external mwrap module repository [mcpplibs/acl-m](https://github.com/mcpplibs/acl-m), package + `mcpplibs.mwrap.acl`, version 2.1.0 (the first indexed release of the identity), tag `mwrap-2.1.0`; release asset + `acl-m-2.1.0.tar.gz` (`b974ddb9…11a4`), CN mirror `gitcode.com/mcpp-res/mwrap-acl` with identical bytes. +- Implementation ownership: this index has no `compat` package of ACL or RTM. The wrapper vendors the headers of both + from their release archives (checksums of every file in `upstream/documents.json`), ACL's `io` directory included + only because `acl/fwd.h` includes its forward declarations (it is not in the module). One upstream fix is applied + (`patches/0001-move-deprecated-attribute.patch`, upstream commit 0b06bd1: an attribute clang 23 refuses where 2.1.0 + places it). It deploys both upstreams' README and LICENSE with its own LICENSE/NOTICE (`runtime.deploy`). +- Configuration: `RTM_NO_INTRINSICS` and `ACL_NO_INTRINSICS`, `-ffp-contract=off`, no fast math, in every profile. + RTM's NEON paths fuse multiply and add and its SSE paths use approximate reciprocals, so a SIMD build gives other + bits on other platforms; the scalar build gives the same compressed bytes and the same poses everywhere. Measured + cost on x86-64 (GCC 16, `-O2`): 26.8 ns a joint to decompress a 64-joint pose, against 17.2 ns with SSE. + +## Module + +- `mcpplibs.mwrap.acl`: every name of the public headers under `acl/` (compression, decompression, core, math) and + `rtm/`, in the namespaces upstream declares them in (`acl`, `rtm`, `rtm::constants`), by `export using` (197 types, + 454 functions). Implementation namespaces (`*_impl`) are not exported. +- Internal-linkage adaptation: 15 `constexpr` objects (`k_invalid_track_index`, `k_num_quality_tiers`, + `k_highest_bit_rate`, `k_bit_rate_num_bits` …) are declared again under their names, with their types and values, + in `mcpplibs::mwrap::acl`. +- No `.macros` module: neither library has macros a program spells. +- 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 five-joint chain compressed twice to the same bytes, its error + measured against the raw clip (below 0.15 mm for a precision of 0.1 mm at 3 cm), poses at five times; the + compressed bytes and the poses pinned and the same on every row; the adapted constants; the export audit; the + default template from an installed copy with a strict build. +- This index: consumer `tests/mwrap/acl-clip` (the same pinned hashes); the installed-template check with + `--deploy` (`STATIC_NOTICES`). diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 48df1233..ffab967d 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -1743,6 +1743,11 @@ jobs: shell: bash run: python3 tests/mwrap/check_installed_template.py --mcpp "$MCPP" --package mcpplibs.mwrap.recastnavigation@1.6.0 --deploy + - name: Installed ACL wrapper archive, actual default clip template and its relocated notices + if: contains(format(' {0} ', env.MEMBERS), ' acl-clip ') && matrix.toolchain == 'default' + shell: bash + run: python3 tests/mwrap/check_installed_template.py --mcpp "$MCPP" --package mcpplibs.mwrap.acl@2.1.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 cb214cef..cabab337 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -41,6 +41,14 @@ ### Added +- **mcpplibs.mwrap.acl 2.1.0(新包)。** acl-m:Animation Compression Library 2.1.0(在给定误差内压缩骨骼动画片段与标量轨道, + 并在任意时刻回放)及其固定的 Realtime Math 2.2.0 的 C++23 模块,以 `export using` 在上游的命名空间 `acl`、`rtm`、`rtm::constants` + 里导出两库头文件的全部公开名字(197 个类型、454 个函数);15 个内部链接的 `constexpr` 常量(`k_invalid_track_index`、 + `k_num_quality_tiers` 等)以原名、原类型和原值位于 `mcpplibs::mwrap::acl`。导出清单由 clang 读头文件推出并在 CI 中审计。封装仓库 + 从上游发布包取源码(应用上游提交 0b06bd1 的一处修复),以 `RTM_NO_INTRINSICS`、`ACL_NO_INTRINSICS`、`-ffp-contract=off`、不用快速 + 数学构建:同一个片段在 Linux x86-64 与 arm64、macOS、Windows、wasm32 上,dev 与 release,压缩字节与回放姿态的哈希相同(钉住)。 + 不含 `io`(SJSON 读写)与引擎插件。消费者测试 `tests/mwrap/acl-clip`,安装后模板检查带 `--deploy`。GLOBAL 为 GitHub 发布资产, + CN 为 GitCode `mcpp-res/mwrap-acl` 镜像,sha256 相同。 - **mcpplibs.mwrap.recastnavigation 1.6.0(新包)。** recastnavigation-m:Recast & Detour 1.6.0(由三角形构建导航网格、寻路、群体、 带障碍物的分块网格)的 C++23 模块,以 `export using` 在全局命名空间导出 Recast、Detour、DetourCrowd、DetourTileCache 头文件的 全部公开名字(99 个类型、75 个无作用域枚举的枚举项、170 个函数);52 个内部链接的 `static const` 常量(`DT_VERTS_PER_POLYGON`、 diff --git a/mcpp.toml b/mcpp.toml index 16d5118b..6a523cc7 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -40,6 +40,7 @@ members = [ "tests/mwrap/manifold-boolean", "tests/mwrap/jolt-world", "tests/mwrap/recastnavigation-path", + "tests/mwrap/acl-clip", "tests/mwrap/basis-universal-transcode", "tests/mwrap/libjpeg-turbo-decode", "tests/mwrap/libarchive-bundle", diff --git a/pkgs/mwrap/acl.lua b/pkgs/mwrap/acl.lua new file mode 100644 index 00000000..f13f01bd --- /dev/null +++ b/pkgs/mwrap/acl.lua @@ -0,0 +1,51 @@ +-- Immutable upstream-aligned mwrap source release (acl-m mwrap-2.1.0, the first release of upstream +-- 2.1.0); GLOBAL and CN bytes are identical. +-- +-- The wrapper is Apache-2.0; the header-only libraries it vendors, the +-- Animation Compression Library 2.1.0 and the Realtime Math 2.2.0 it pins, +-- are MIT: the package deploys their READMEs and LICENSEs beside programs +-- (runtime.deploy). +package = { + spec = "1", + namespace = "mcpplibs.mwrap", + name = "acl", + description = "acl-m: the Animation Compression Library 2.1.0 and Realtime Math 2.2.0 (skeletal clips compressed within a stated error, played back at any time) as a C++23 module", + licenses = { "Apache-2.0" }, + repo = "https://github.com/mcpplibs/acl-m", + type = "package", + extensions = { + autoupdate = { + tag = "mwrap-${version}", + url = "https://github.com/mcpplibs/acl-m/releases/download/${tag}/acl-m-${version}.tar.gz", + }, + }, + xpm = { + linux = { + ["2.1.0"] = { + url = { + GLOBAL = "https://github.com/mcpplibs/acl-m/releases/download/mwrap-2.1.0/acl-m-2.1.0.tar.gz", + CN = "https://gitcode.com/mcpp-res/mwrap-acl/releases/download/2.1.0/acl-m-2.1.0.tar.gz", + }, + sha256 = "b974ddb90229593ce20dc8ef86f529720bc7983e09700db8cd67a326069a11a4", + }, + }, + macosx = { + ["2.1.0"] = { + url = { + GLOBAL = "https://github.com/mcpplibs/acl-m/releases/download/mwrap-2.1.0/acl-m-2.1.0.tar.gz", + CN = "https://gitcode.com/mcpp-res/mwrap-acl/releases/download/2.1.0/acl-m-2.1.0.tar.gz", + }, + sha256 = "b974ddb90229593ce20dc8ef86f529720bc7983e09700db8cd67a326069a11a4", + }, + }, + windows = { + ["2.1.0"] = { + url = { + GLOBAL = "https://github.com/mcpplibs/acl-m/releases/download/mwrap-2.1.0/acl-m-2.1.0.tar.gz", + CN = "https://gitcode.com/mcpp-res/mwrap-acl/releases/download/2.1.0/acl-m-2.1.0.tar.gz", + }, + sha256 = "b974ddb90229593ce20dc8ef86f529720bc7983e09700db8cd67a326069a11a4", + }, + }, + }, +} diff --git a/tests/mwrap/acl-clip/mcpp.toml b/tests/mwrap/acl-clip/mcpp.toml new file mode 100644 index 00000000..97ed1bb7 --- /dev/null +++ b/tests/mwrap/acl-clip/mcpp.toml @@ -0,0 +1,15 @@ +[package] +name = "acl-clip" +version = "2.1.0" +description = "Consume the immutable ACL wrapper: a five-joint chain compressed within its error twice to the same bytes and played back at five times; the compressed bytes and the poses pinned" + +[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.acl = "2.1.0" + +# RTM's inline math compiled here too, as the library is built. +[build] +cxxflags = ["-ffp-contract=off"] diff --git a/tests/mwrap/acl-clip/tests/behavior.cpp b/tests/mwrap/acl-clip/tests/behavior.cpp new file mode 100644 index 00000000..d7675575 --- /dev/null +++ b/tests/mwrap/acl-clip/tests/behavior.cpp @@ -0,0 +1,90 @@ +// The installed ACL wrapper: a chain of five joints turning and moving over two seconds at 30 samples a second, +// compressed with ACL's default settings twice to the same bytes, its error measured against the raw clip, and +// played back at five times. The compressed bytes and the poses are pinned, as the wrapper's CI pins them on every +// platform; the adapted constants. +import std; +import mcpplibs.mwrap.acl; + +namespace { + +int failures = 0; +void check(bool held) { failures += held ? 0 : 1; } + +constexpr std::uint32_t joints = 5, samples = 61; +constexpr float rate = 30.0f; + +std::uint64_t fnv(const void* data, std::size_t size, std::uint64_t hash = 0xcbf29ce484222325ull) { + const auto* bytes = static_cast(data); + for (std::size_t index = 0; index < size; ++index) hash = (hash ^ bytes[index]) * 0x100000001b3ull; + return hash; +} + +// Each joint's rotation a normalised quaternion of polynomials in time, its translation along its parent's x. +acl::track_array_qvvf raw(acl::iallocator& allocator) { + acl::track_array_qvvf tracks(allocator, joints); + for (std::uint32_t joint = 0; joint < joints; ++joint) { + acl::track_desc_transformf desc; + desc.output_index = joint; + desc.parent_index = joint == 0 ? mcpplibs::mwrap::acl::k_invalid_track_index : joint - 1; + desc.precision = 0.0001f; + desc.shell_distance = 0.03f; + acl::track_qvvf track = acl::track_qvvf::make_reserve(desc, allocator, samples, rate); + for (std::uint32_t sample = 0; sample < samples; ++sample) { + const float time = static_cast(sample) / rate, turn = 0.3f * time - 0.1f * time * time + 0.05f * static_cast(joint); + const rtm::quatf rotation = rtm::quat_normalize(rtm::quat_set(turn, 0.2f * turn * turn, 0.1f * static_cast(joint), 1.0f)); + const rtm::vector4f translation = rtm::vector_set(joint == 0 ? 0.5f * time : 0.25f, joint == 0 ? 0.1f * time * time : 0.0f, 0.0f); + track[sample] = rtm::qvv_set(rotation, translation, rtm::vector_set(1.0f)); + } + tracks[joint] = std::move(track); + } + return tracks; +} + +struct Pose : acl::track_writer { + std::array values{}; + void write_rotation(std::uint32_t joint, rtm::quatf_arg0 rotation) { rtm::quat_store(rotation, &values[joint * 10]); } + void write_translation(std::uint32_t joint, rtm::vector4f_arg0 translation) { rtm::vector_store3(translation, &values[joint * 10 + 4]); } + void write_scale(std::uint32_t joint, rtm::vector4f_arg0 scale) { rtm::vector_store3(scale, &values[joint * 10 + 7]); } +}; + +} // namespace + +int main() { + acl::ansi_allocator allocator; + const acl::track_array_qvvf tracks = raw(allocator); + acl::compression_settings settings = acl::get_default_compression_settings(); + acl::qvvf_transform_error_metric metric; + settings.error_metric = &metric; + const auto compress = [&] { + acl::output_stats stats; + acl::compressed_tracks* compressed = nullptr; + check(acl::compress_track_list(allocator, tracks, settings, compressed, stats).empty() && compressed); + return compressed; + }; + acl::compressed_tracks* compressed = compress(); + acl::compressed_tracks* again = compress(); + if (!compressed || !again) return 1; + check(compressed->is_valid(true).empty()); + check(compressed->get_size() == again->get_size() && std::memcmp(compressed, again, compressed->get_size()) == 0); + + acl::decompression_context context; + check(context.initialize(*compressed)); + const acl::track_error error = acl::calculate_compression_error(allocator, tracks, context, metric); + check(error.error < 0.00015f); + std::uint64_t poses = 0xcbf29ce484222325ull; + for (const float time : {0.0f, 0.37f, 1.0f, 1.5f, 2.0f}) { + Pose pose; + context.seek(time, acl::sample_rounding_policy::none); + context.decompress_tracks(pose); + poses = fnv(pose.values.data(), sizeof(pose.values), poses); + } + const std::uint64_t bytes = fnv(compressed, compressed->get_size()); + check(bytes == 0x7bbb0daa9b0e5fa6ull && poses == 0x0c6e1a02a38a29a5ull); + check(mcpplibs::mwrap::acl::k_invalid_track_index == 0xFFFFFFFFu && mcpplibs::mwrap::acl::k_num_quality_tiers == 3u); + std::println("ACL 2.1.0 through import mcpplibs.mwrap.acl: {} raw bytes compressed to {} within {:.3f} mm; the bytes {:#018x}, the poses {:#018x}{}", + joints * samples * 10 * sizeof(float), compressed->get_size(), error.error * 1000.0f, bytes, poses, + failures ? std::format(" — {} checks failed", failures) : ""); + allocator.deallocate(compressed, compressed->get_size()); + allocator.deallocate(again, again->get_size()); + return failures; +} diff --git a/tests/mwrap/check_installed_template.py b/tests/mwrap/check_installed_template.py index 66da69ef..02e9922d 100644 --- a/tests/mwrap/check_installed_template.py +++ b/tests/mwrap/check_installed_template.py @@ -24,6 +24,7 @@ "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.recastnavigation": ("recastnavigation.cppm", "upstream/recastnavigation/License.txt", b"This software is provided 'as-is'", b"Recast & Detour is Copyright (c) 2009 Mikko"), + "mcpplibs.mwrap.acl": ("acl.cppm", "upstream/acl/LICENSE", b"Permission is hereby granted", b"Copyright (c) 2017 Nicholas Frechette & Animation Compression Library"), "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"),