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
42 changes: 42 additions & 0 deletions .agents/docs/2026-11-15-add-acl-mwrap-plan.md
Original file line number Diff line number Diff line change
@@ -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`).
5 changes: 5 additions & 0 deletions .github/workflows/validate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`、
Expand Down
1 change: 1 addition & 0 deletions mcpp.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
51 changes: 51 additions & 0 deletions pkgs/mwrap/acl.lua
Original file line number Diff line number Diff line change
@@ -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",
},
},
},
}
15 changes: 15 additions & 0 deletions tests/mwrap/acl-clip/mcpp.toml
Original file line number Diff line number Diff line change
@@ -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"]
90 changes: 90 additions & 0 deletions tests/mwrap/acl-clip/tests/behavior.cpp
Original file line number Diff line number Diff line change
@@ -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<const unsigned char*>(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<float>(sample) / rate, turn = 0.3f * time - 0.1f * time * time + 0.05f * static_cast<float>(joint);
const rtm::quatf rotation = rtm::quat_normalize(rtm::quat_set(turn, 0.2f * turn * turn, 0.1f * static_cast<float>(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<float, joints * 10> 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<acl::default_transform_decompression_settings> 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;
}
1 change: 1 addition & 0 deletions tests/mwrap/check_installed_template.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand Down
Loading