diff --git a/.agents/docs/2026-11-14-add-meshoptimizer-cgltf-mwrap-plan.md b/.agents/docs/2026-11-14-add-meshoptimizer-cgltf-mwrap-plan.md new file mode 100644 index 00000000..1f3e8557 --- /dev/null +++ b/.agents/docs/2026-11-14-add-meshoptimizer-cgltf-mwrap-plan.md @@ -0,0 +1,40 @@ +# Add mcpplibs.mwrap.meshoptimizer (meshoptimizer-m 1.3) and mcpplibs.mwrap.cgltf (cgltf-m 1.15) + +## Form + +- Third-party upstreams [zeux/meshoptimizer](https://github.com/zeux/meshoptimizer), release v1.3, MIT, and + [jkuhlmann/cgltf](https://github.com/jkuhlmann/cgltf), release v1.15, MIT (it embeds jsmn, MIT). +- Form A external mwrap module repositories [mcpplibs/meshoptimizer-m](https://github.com/mcpplibs/meshoptimizer-m) and + [mcpplibs/cgltf-m](https://github.com/mcpplibs/cgltf-m); packages `mcpplibs.mwrap.meshoptimizer` 1.3 and + `mcpplibs.mwrap.cgltf` 1.15 (first releases of both identities), tags `mwrap-1.3` and `mwrap-1.15`; release assets + `meshoptimizer-m-1.3.tar.gz` (`5172030f…4e7e`) and `cgltf-m-1.15.tar.gz` (`242d90c6…070b`), CN mirrors + `gitcode.com/mcpp-res/mwrap-meshoptimizer` and `gitcode.com/mcpp-res/mwrap-cgltf` with identical bytes. +- Implementation ownership: neither library has a compat package in this index, so each wrapper vendors its upstream + from the release archive (checksums in `upstream/documents.json`) and builds it itself: meshoptimizer's `src/*.cpp` + unchanged; cgltf's two headers unchanged, their implementation in one C translation unit of the package + (`port/cgltf.c`) with upstream's defaults. Both deploy upstream's README and license with their own LICENSE/NOTICE + (`runtime.deploy`). + +## Modules + +- `mcpplibs.mwrap.meshoptimizer`: every public name of `` (9 types, 20 enumerators, 89 functions) by + `export using`, in the global namespace where upstream declares them; `mcpplibs.mwrap.meshoptimizer.macros`: + `MESHOPTIMIZER_VERSION` as a constant. +- `mcpplibs.mwrap.cgltf`: every public name of `` (72 types, 102 enumerators, 37 functions), re-exporting + `mcpplibs.mwrap.cgltf.write` (``: `cgltf_write`, `cgltf_write_file`). Neither header's public part + has an object-like constant, so there is no `.macros` module; the configuration macros (`CGLTF_MALLOC` …) are fixed + at upstream's defaults by the package's one implementation. +- Both export lists are derived from the headers by each repository's `tools/surface.py` and audited in CI + (`tools/check_exports.py`). + +## Verification + +- Wrapper CI (Linux x86_64 GCC 16.1 and LLVM 23.1.3, macOS LLVM, Windows LLVM): the module tests, header mixing, the + export check, and the default template from an installed copy with a strict build. +- This index: consumers `tests/mwrap/meshoptimizer-simplify` (indexing, vertex cache order, simplification, meshlets, + both codecs) and `tests/mwrap/cgltf-inspect` (parse, load, validate, accessors, a node's transform, the writer, the + reasons broken documents are refused with); the installed-template check with `--deploy` for both + (`STATIC_NOTICES`). +- `tests/mwrap/check_installed_template.py` reads the generated manifest's dependency whether its key is quoted or + dotted: both templates write the dotted form (mcpp 2026.11.8.1 warns on a quoted key that needs none, and the check + builds with `--strict`). diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index be01d306..37297a34 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -1718,6 +1718,16 @@ jobs: shell: bash run: python3 tests/mwrap/check_installed_template.py --mcpp "$MCPP" --package mcpplibs.mwrap.xxhash@0.8.3 --deploy + - name: Installed meshoptimizer wrapper archive, actual default simplification template and its relocated notices + if: contains(format(' {0} ', env.MEMBERS), ' meshoptimizer-simplify ') && matrix.toolchain == 'default' + shell: bash + run: python3 tests/mwrap/check_installed_template.py --mcpp "$MCPP" --package mcpplibs.mwrap.meshoptimizer@1.3 --deploy + + - name: Installed cgltf wrapper archive, actual default inspection template and its relocated notices + if: contains(format(' {0} ', env.MEMBERS), ' cgltf-inspect ') && matrix.toolchain == 'default' + shell: bash + run: python3 tests/mwrap/check_installed_template.py --mcpp "$MCPP" --package mcpplibs.mwrap.cgltf@1.15 --deploy + - name: Installed libarchive wrapper archive, actual default archive template and its relocated notices if: contains(format(' {0} ', env.MEMBERS), ' libarchive-bundle ') && matrix.toolchain == 'default' shell: bash diff --git a/CHANGELOG.md b/CHANGELOG.md index f8fe8d29..3c445f85 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,6 +38,13 @@ ### Added +- **mcpplibs.mwrap.meshoptimizer 1.3、mcpplibs.mwrap.cgltf 1.15(新包)。** 两个 mwrap 包:meshoptimizer-m 以 `export using` + 导出 `` 的全部公开名字(9 个类型、20 个枚举值、89 个函数),`MESHOPTIMIZER_VERSION` 在 `.macros` 中为同名常量; + cgltf-m 导出 `` 的全部公开名字(72 个类型、102 个枚举值、37 个函数),并再导出 `.write`(``)。 + 本索引没有这两个库的 compat 包,封装仓库各自从上游发布包取源码(校验和在 `upstream/documents.json`)并自行构建;导出清单由 + 各仓库的 `tools/surface.py` 从头文件推出并在 CI 中审计。消费者测试 `tests/mwrap/meshoptimizer-simplify`、 + `tests/mwrap/cgltf-inspect`,安装后模板检查带 `--deploy`(`STATIC_NOTICES`);该检查读取生成的清单时同时接受带引号与 + 点分两种依赖键。GLOBAL 为 GitHub 发布资产,CN 为 GitCode `mcpp-res/mwrap-` 镜像,sha256 相同。 - **mtest 0.8.3.** 四个包的 0.8.3(同一标签、同一归档;含未入索引的 0.8.1、0.8.2 的内容):`bench::each` 的一次调用可以说明自己的组成 (`part`、`time`、`value`),给出最慢的几次、超过上限的次数与分配计数;`bench::laps` 是同一测量的循环形式; 程序单独运行时打印测得的数字,并按标识、名字或带 `*` 的模式选择用例。只增不改。 diff --git a/docs/mwrap.md b/docs/mwrap.md index 7c9ffc27..6260f2f7 100644 --- a/docs/mwrap.md +++ b/docs/mwrap.md @@ -150,6 +150,9 @@ Templates are data, without execution hooks. With the pinned current mcpp, ``` Quote the selector as one key; do not place it under another namespace table. +From mcpp 2026.11.8.1 a quoted key that needs no quotes is reported (and `--strict` refuses it); a template +tested with that client writes the same selector as a dotted key, `{{self.name}} = "{{self.version}}"`, which +names the same package. `tests/mwrap/check_installed_template.py` reads either form. Declare required features explicitly and keep the library version tied to `{{self.version}}`. Test actual `mcpp new`, then build and run the generated project. A source candidate may use an isolated preseeded installed-package diff --git a/docs/zh/mwrap.md b/docs/zh/mwrap.md index 8c2a343e..edc497cd 100644 --- a/docs/zh/mwrap.md +++ b/docs/zh/mwrap.md @@ -97,7 +97,9 @@ mcpp new --list-templates mcpplibs.mwrap.opencv@5.0.0 mcpp new viewer --template mcpplibs.mwrap.opencv@5.0.0:window ``` -模板是纯数据,不执行 hook。当前 mcpp 的 self.name 为完整包选择器,应作为一个带引号的 key: +模板是纯数据,不执行 hook。当前 mcpp 的 self.name 为完整包选择器,应作为一个带引号的 key(mcpp 2026.11.8.1 起,不需要引号的 +key 带引号会被提示,`--strict` 下被拒绝;以该版本测试的模板把同一选择器写成点分 key `{{self.name}} = "{{self.version}}"`, +指向同一个包,`tests/mwrap/check_installed_template.py` 两种写法都读): ```toml [dependencies] diff --git a/mcpp.toml b/mcpp.toml index c10bbb10..30f75505 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -35,6 +35,8 @@ members = [ "tests/mwrap/zstd-squeeze", "tests/mwrap/xz-pack", "tests/mwrap/xxhash-digest", + "tests/mwrap/meshoptimizer-simplify", + "tests/mwrap/cgltf-inspect", "tests/mwrap/libarchive-bundle", "tests/mwrap/libc-stdio", "tests/mwrap/miniaudio-engine", diff --git a/pkgs/mwrap/cgltf.lua b/pkgs/mwrap/cgltf.lua new file mode 100644 index 00000000..8b228229 --- /dev/null +++ b/pkgs/mwrap/cgltf.lua @@ -0,0 +1,50 @@ +-- Immutable upstream-aligned mwrap source release (cgltf-m mwrap-1.15); +-- GLOBAL and CN bytes are identical. +-- +-- The wrapper is Apache-2.0; the library it vendors and links statically, +-- cgltf 1.15 (it embeds jsmn), is MIT: the package deploys its README and +-- LICENSE beside programs (runtime.deploy). +package = { + spec = "1", + namespace = "mcpplibs.mwrap", + name = "cgltf", + description = "cgltf-m: cgltf 1.15 (glTF 2.0 loader and writer: parsing, buffers, validation, accessors, node transforms) as C++23 modules", + licenses = { "Apache-2.0" }, + repo = "https://github.com/mcpplibs/cgltf-m", + type = "package", + extensions = { + autoupdate = { + tag = "mwrap-${version}", + url = "https://github.com/mcpplibs/cgltf-m/releases/download/${tag}/cgltf-m-${version}.tar.gz", + }, + }, + xpm = { + linux = { + ["1.15"] = { + url = { + GLOBAL = "https://github.com/mcpplibs/cgltf-m/releases/download/mwrap-1.15/cgltf-m-1.15.tar.gz", + CN = "https://gitcode.com/mcpp-res/mwrap-cgltf/releases/download/1.15/cgltf-m-1.15.tar.gz", + }, + sha256 = "242d90c65178d0746a25a22b375984bf95af19d9820f7d91b0fb32a06d51070b", + }, + }, + macosx = { + ["1.15"] = { + url = { + GLOBAL = "https://github.com/mcpplibs/cgltf-m/releases/download/mwrap-1.15/cgltf-m-1.15.tar.gz", + CN = "https://gitcode.com/mcpp-res/mwrap-cgltf/releases/download/1.15/cgltf-m-1.15.tar.gz", + }, + sha256 = "242d90c65178d0746a25a22b375984bf95af19d9820f7d91b0fb32a06d51070b", + }, + }, + windows = { + ["1.15"] = { + url = { + GLOBAL = "https://github.com/mcpplibs/cgltf-m/releases/download/mwrap-1.15/cgltf-m-1.15.tar.gz", + CN = "https://gitcode.com/mcpp-res/mwrap-cgltf/releases/download/1.15/cgltf-m-1.15.tar.gz", + }, + sha256 = "242d90c65178d0746a25a22b375984bf95af19d9820f7d91b0fb32a06d51070b", + }, + }, + }, +} diff --git a/pkgs/mwrap/meshoptimizer.lua b/pkgs/mwrap/meshoptimizer.lua new file mode 100644 index 00000000..1eed8b5b --- /dev/null +++ b/pkgs/mwrap/meshoptimizer.lua @@ -0,0 +1,50 @@ +-- Immutable upstream-aligned mwrap source release (meshoptimizer-m mwrap-1.3); +-- GLOBAL and CN bytes are identical. +-- +-- The wrapper is Apache-2.0; the library it vendors and links statically, +-- meshoptimizer 1.3, is MIT: the package deploys its README and LICENSE beside +-- programs (runtime.deploy). +package = { + spec = "1", + namespace = "mcpplibs.mwrap", + name = "meshoptimizer", + description = "meshoptimizer-m: meshoptimizer 1.3 (indexing, vertex cache and overdraw ordering, simplification, meshlets, vertex and index codecs) as C++23 modules", + licenses = { "Apache-2.0" }, + repo = "https://github.com/mcpplibs/meshoptimizer-m", + type = "package", + extensions = { + autoupdate = { + tag = "mwrap-${version}", + url = "https://github.com/mcpplibs/meshoptimizer-m/releases/download/${tag}/meshoptimizer-m-${version}.tar.gz", + }, + }, + xpm = { + linux = { + ["1.3"] = { + url = { + GLOBAL = "https://github.com/mcpplibs/meshoptimizer-m/releases/download/mwrap-1.3/meshoptimizer-m-1.3.tar.gz", + CN = "https://gitcode.com/mcpp-res/mwrap-meshoptimizer/releases/download/1.3/meshoptimizer-m-1.3.tar.gz", + }, + sha256 = "5172030f7b27b757cff2945d661e12969a191b08ed8b8ce19e70220fd3654e7e", + }, + }, + macosx = { + ["1.3"] = { + url = { + GLOBAL = "https://github.com/mcpplibs/meshoptimizer-m/releases/download/mwrap-1.3/meshoptimizer-m-1.3.tar.gz", + CN = "https://gitcode.com/mcpp-res/mwrap-meshoptimizer/releases/download/1.3/meshoptimizer-m-1.3.tar.gz", + }, + sha256 = "5172030f7b27b757cff2945d661e12969a191b08ed8b8ce19e70220fd3654e7e", + }, + }, + windows = { + ["1.3"] = { + url = { + GLOBAL = "https://github.com/mcpplibs/meshoptimizer-m/releases/download/mwrap-1.3/meshoptimizer-m-1.3.tar.gz", + CN = "https://gitcode.com/mcpp-res/mwrap-meshoptimizer/releases/download/1.3/meshoptimizer-m-1.3.tar.gz", + }, + sha256 = "5172030f7b27b757cff2945d661e12969a191b08ed8b8ce19e70220fd3654e7e", + }, + }, + }, +} diff --git a/tests/mwrap/cgltf-inspect/mcpp.toml b/tests/mwrap/cgltf-inspect/mcpp.toml new file mode 100644 index 00000000..60de10f3 --- /dev/null +++ b/tests/mwrap/cgltf-inspect/mcpp.toml @@ -0,0 +1,11 @@ +[package] +name = "cgltf-inspect" +version = "1.15" +description = "Consume the immutable cgltf wrapper: parse a glTF document, load and validate its buffer, read its accessors, write it and parse it again" + +[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.cgltf = "1.15" diff --git a/tests/mwrap/cgltf-inspect/tests/behavior.cpp b/tests/mwrap/cgltf-inspect/tests/behavior.cpp new file mode 100644 index 00000000..71053f25 --- /dev/null +++ b/tests/mwrap/cgltf-inspect/tests/behavior.cpp @@ -0,0 +1,64 @@ +// The installed cgltf wrapper: a triangle whose buffer is in the document, +// parsed, loaded, validated and read; a broken document refused with its +// reason; the writer's output parsed again. +import std; +import mcpplibs.mwrap.cgltf; + +namespace { + +// Three positions (36 bytes of floats) and three 16-bit indices (6 bytes, padded to 44), as base64. +constexpr std::string_view triangle = R"({ + "asset": {"version": "2.0"}, + "scene": 0, + "scenes": [{"nodes": [0]}], + "nodes": [{"name": "a triangle", "mesh": 0, "translation": [1, 2, 3]}], + "meshes": [{"name": "tri", "primitives": [{"attributes": {"POSITION": 0}, "indices": 1, "material": 0}]}], + "materials": [{"name": "red", "pbrMetallicRoughness": {"baseColorFactor": [1, 0, 0, 1], "metallicFactor": 0.25}}], + "buffers": [{"byteLength": 44, "uri": "data:application/octet-stream;base64,AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAABAAIAAAA="}], + "bufferViews": [{"buffer": 0, "byteLength": 36}, {"buffer": 0, "byteOffset": 36, "byteLength": 6}], + "accessors": [ + {"bufferView": 0, "componentType": 5126, "count": 3, "type": "VEC3", "min": [0, 0, 0], "max": [1, 1, 0]}, + {"bufferView": 1, "componentType": 5123, "count": 3, "type": "SCALAR"} + ] +})"; + +} // namespace + +int main() { + const cgltf_options options{}; + cgltf_data* data = nullptr; + if (cgltf_parse(&options, triangle.data(), triangle.size(), &data) != cgltf_result_success) return 1; + if (cgltf_load_buffers(&options, data, nullptr) != cgltf_result_success || cgltf_validate(data) != cgltf_result_success) return 2; + if (data->meshes_count != 1 || data->nodes[0].mesh != &data->meshes[0] || std::string_view(data->nodes[0].name) != "a triangle") return 3; + + const cgltf_primitive& primitive = data->meshes[0].primitives[0]; + const cgltf_accessor* positions = cgltf_find_accessor(&primitive, cgltf_attribute_type_position, 0); + std::array corners{}; + if (!positions || cgltf_accessor_unpack_floats(positions, corners.data(), corners.size()) != 9) return 4; + if (corners != std::array{0, 0, 0, 1, 0, 0, 0, 1, 0}) return 5; + if (!primitive.indices || cgltf_accessor_read_index(primitive.indices, 2) != 2) return 6; + if (!primitive.material || primitive.material->pbr_metallic_roughness.metallic_factor != 0.25f) return 7; + std::array world{}; + cgltf_node_transform_world(&data->nodes[0], world.data()); + if (world[12] != 1 || world[13] != 2 || world[14] != 3) return 8; + + // The writer (mcpplibs.mwrap.cgltf.write, re-exported) gives a document that parses the same. + std::string written(cgltf_write(&options, nullptr, 0, data), '\0'); + if (written.empty() || cgltf_write(&options, written.data(), written.size(), data) != written.size()) return 9; + cgltf_data* again = nullptr; + if (cgltf_parse(&options, written.data(), written.size() - 1, &again) != cgltf_result_success) return 10; + const bool same = again->accessors_count == 2 && std::string_view(again->materials[0].name) == "red" && again->nodes[0].translation[2] == 3; + cgltf_free(again); + cgltf_free(data); + if (!same) return 11; + + // What is not glTF is refused with its reason. + const std::string_view broken = R"({"asset": {"version": "2.0"}, "meshes": [)"; + const std::string_view past = R"({"asset": {"version": "2.0"}, "nodes": [{"mesh": 3}]})"; + cgltf_data* none = nullptr; + if (cgltf_parse(&options, broken.data(), broken.size(), &none) != cgltf_result_invalid_json) return 12; + if (cgltf_parse(&options, past.data(), past.size(), &none) != cgltf_result_invalid_gltf) return 13; + + std::println("cgltf 1.15 through import mcpplibs.mwrap.cgltf: a triangle of {} positions read, written in {} bytes and parsed again", corners.size() / 3, written.size() - 1); + return 0; +} diff --git a/tests/mwrap/check_installed_template.py b/tests/mwrap/check_installed_template.py index 360d6a76..329be094 100644 --- a/tests/mwrap/check_installed_template.py +++ b/tests/mwrap/check_installed_template.py @@ -20,6 +20,8 @@ "mcpplibs.mwrap.xz": ("xz.cppm", "upstream/xz/COPYING.0BSD", b"Permission to use, copy, modify", b"XZ Utils authors"), "mcpplibs.mwrap.xxhash": ("xxhash.cppm", "upstream/xxhash/LICENSE", b"BSD 2-Clause", b"Yann Collet"), "mcpplibs.mwrap.libarchive": ("libarchive.cppm", "upstream/libarchive/COPYING", b"Tim Kientzle", b"Tim Kientzle"), + "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.libc": ("libc.cppm", None, b"", b"ISO/IEC 9899:2018"), } @@ -59,7 +61,12 @@ def main(): subprocess.run(command, cwd=temporary, env=env, check=True) project = temporary / "installed-quickstart" manifest = tomllib.loads((project / "mcpp.toml").read_text(encoding="utf-8")) - dependency = manifest["dependencies"][identity] + # The key as written: quoted (one key) or dotted (nested tables), the form mcpp 2026.11.8.1 asks for. + dependency = manifest["dependencies"].get(identity) + if dependency is None: + dependency = manifest["dependencies"] + for part in identity.split("."): + dependency = dependency[part] actual_version = dependency if isinstance(dependency, str) else dependency["version"] assert actual_version == version, dependency if isinstance(dependency, dict): diff --git a/tests/mwrap/meshoptimizer-simplify/mcpp.toml b/tests/mwrap/meshoptimizer-simplify/mcpp.toml new file mode 100644 index 00000000..31422da0 --- /dev/null +++ b/tests/mwrap/meshoptimizer-simplify/mcpp.toml @@ -0,0 +1,11 @@ +[package] +name = "meshoptimizer-simplify" +version = "1.3" +description = "Consume the immutable meshoptimizer wrapper: index a grid, order it for the vertex cache, simplify it, cut meshlets, and round-trip both codecs" + +[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.meshoptimizer = "1.3" diff --git a/tests/mwrap/meshoptimizer-simplify/tests/behavior.cpp b/tests/mwrap/meshoptimizer-simplify/tests/behavior.cpp new file mode 100644 index 00000000..9bc2649f --- /dev/null +++ b/tests/mwrap/meshoptimizer-simplify/tests/behavior.cpp @@ -0,0 +1,76 @@ +// The installed meshoptimizer wrapper: a grid of quads indexed, ordered for +// the vertex cache, simplified, cut into meshlets, and through both codecs. +import std; +import mcpplibs.mwrap.meshoptimizer; + +namespace macros = mcpplibs::mwrap::meshoptimizer::macros; + +namespace { + +struct Vertex { + float x, y, z; +}; + +} // namespace + +int main() { + // 16 × 16 quads on a plane, each corner of each triangle its own vertex. + constexpr unsigned side = 16; + std::vector corners; + for (unsigned row = 0; row < side; ++row) + for (unsigned column = 0; column < side; ++column) + for (const auto& [dx, dy] : {std::pair{0.f, 0.f}, {1.f, 0.f}, {1.f, 1.f}, {0.f, 0.f}, {1.f, 1.f}, {0.f, 1.f}}) + corners.push_back({static_cast(column) + dx, static_cast(row) + dy, 0}); + + std::vector remap(corners.size()); + const std::size_t unique = meshopt_generateVertexRemap(remap.data(), nullptr, corners.size(), corners.data(), corners.size(), sizeof(Vertex)); + if (unique != (side + 1) * (side + 1)) return 1; + std::vector vertices(unique); + std::vector indices(corners.size()); + meshopt_remapIndexBuffer(indices.data(), nullptr, corners.size(), remap.data()); + meshopt_remapVertexBuffer(vertices.data(), corners.data(), corners.size(), sizeof(Vertex), remap.data()); + + const float before = meshopt_analyzeVertexCache(indices.data(), indices.size(), unique, 16, 0, 0).acmr; + meshopt_optimizeVertexCache(indices.data(), indices.data(), indices.size(), unique); + if (meshopt_analyzeVertexCache(indices.data(), indices.size(), unique, 16, 0, 0).acmr > before) return 2; + + // A flat grid loses nearly all its triangles at no error. + std::vector fewer(indices.size()); + float error = 1; + fewer.resize(meshopt_simplify(fewer.data(), indices.data(), indices.size(), &vertices[0].x, unique, sizeof(Vertex), 6, 1e-3f, 0, &error)); + if (fewer.size() < 6 || fewer.size() > indices.size() / 4 || fewer.size() % 3 != 0 || error > 1e-3f) return 3; + + // Meshlets hold every triangle once. + constexpr std::size_t most_vertices = 64, most_triangles = 96; + std::vector meshlets(meshopt_buildMeshletsBound(indices.size(), most_vertices, most_triangles)); + std::vector meshlet_vertices(indices.size()); + std::vector meshlet_triangles(indices.size()); + meshlets.resize(meshopt_buildMeshlets(meshlets.data(), meshlet_vertices.data(), meshlet_triangles.data(), indices.data(), indices.size(), &vertices[0].x, unique, + sizeof(Vertex), most_vertices, most_triangles, 0)); + std::size_t triangles = 0; + for (const meshopt_Meshlet& meshlet : meshlets) triangles += meshlet.triangle_count; + if (triangles != indices.size() / 3) return 4; + + // Both codecs give back the bytes they were given. + std::vector packed_vertices(meshopt_encodeVertexBufferBound(unique, sizeof(Vertex))); + packed_vertices.resize(meshopt_encodeVertexBuffer(packed_vertices.data(), packed_vertices.size(), vertices.data(), unique, sizeof(Vertex))); + std::vector unpacked_vertices(unique); + if (meshopt_decodeVertexBuffer(unpacked_vertices.data(), unique, sizeof(Vertex), packed_vertices.data(), packed_vertices.size()) != 0) return 5; + if (std::memcmp(unpacked_vertices.data(), vertices.data(), unique * sizeof(Vertex)) != 0) return 6; + std::vector packed_indices(meshopt_encodeIndexBufferBound(indices.size(), unique)); + packed_indices.resize(meshopt_encodeIndexBuffer(packed_indices.data(), packed_indices.size(), indices.data(), indices.size())); + std::vector unpacked_indices(indices.size()); + if (meshopt_decodeIndexBuffer(unpacked_indices.data(), indices.size(), sizeof(unsigned int), packed_indices.data(), packed_indices.size()) != 0) return 7; + // The index codec keeps each triangle and their order; a triangle may come back turned (b c a for a b c). + for (std::size_t at = 0; at < indices.size(); at += 3) { + const std::array was{indices[at], indices[at + 1], indices[at + 2]}; + bool same = false; + for (std::size_t turn = 0; turn < 3; ++turn) + same = same || (unpacked_indices[at] == was[turn] && unpacked_indices[at + 1] == was[(turn + 1) % 3] && unpacked_indices[at + 2] == was[(turn + 2) % 3]); + if (!same) return 8; + } + + std::println("meshoptimizer {} through import mcpplibs.mwrap.meshoptimizer: {} vertices of {} corners, {} of {} triangles kept, {} meshlets, vertices in {} bytes of {}", + macros::MESHOPTIMIZER_VERSION, unique, corners.size(), fewer.size() / 3, indices.size() / 3, meshlets.size(), packed_vertices.size(), unique * sizeof(Vertex)); + return macros::MESHOPTIMIZER_VERSION == 1030 ? 0 : 9; +}