diff --git a/.agents/docs/2026-08-12-add-harfbuzz-msdfgen-mimalloc-plan.md b/.agents/docs/2026-08-12-add-harfbuzz-msdfgen-mimalloc-plan.md new file mode 100644 index 0000000..cd50c66 --- /dev/null +++ b/.agents/docs/2026-08-12-add-harfbuzz-msdfgen-mimalloc-plan.md @@ -0,0 +1,166 @@ +# 新增 compat.harfbuzz / compat.msdfgen / compat.mimalloc + +> 日期:2026-08-12 · 验证工具链:mcpp `2026.8.10.3`(与 `validate.yml` 的 `MCPP_VERSION` 对齐)+ gcc `16.1.0` + +## 1. 动机 + +这三个库是把 [XRGUI](https://github.com/Sunrisepeak/xrgui)(C++23 模块化保留式 GUI 库) +适配到 mcpp 时,索引里**唯一还缺的三个编译型依赖**。适配记录见 +[Sunrisepeak/xrgui#1](https://github.com/Sunrisepeak/xrgui/pull/1):该项目的源码符合性问题 +已全部清零,根包构建停在且仅停在这三行: + +``` +fatal error: mimalloc.h: No such file or directory +fatal error: hb.h: No such file or directory +fatal error: msdfgen/msdfgen-ext.h: No such file or directory +``` + +三者本身也都是通用库(分配器 / 文本整形 / SDF 生成),不是为单个下游定制的。 + +## 2. 形态判定 + +| 包 | 形态 | 判定依据 | +|---|---|---| +| `compat.mimalloc` | **A**(C 源码 compat) | 纯 C,18 个 TU,用户写 `#include ` | +| `compat.harfbuzz` | **A**(C++ 版) | 上游自带 amalgamation,单 TU | +| `compat.msdfgen` | **A + E** | C++ 源码,且**必须**生成 `msdfgen-config.h` | + +### 2.1 mimalloc —— 为什么源列表是枚举而非通配 + +`src/*.c` 会在三个地方出错,且**每一处都是链接期而非编译期报错**: + +- `src/static.c` 是 amalgamation,`#include` 了整个库,与其它文件同时编入会让每个符号重复; +- `src/free.c` 被 `alloc.c` 第 22 行 `#include`,不是独立 TU; +- `src/alloc-override.c` 同理(`alloc.c:21`)。 + +所以源列表取上游自己的 `mi_sources`(`CMakeLists.txt:75-93`)—— 那是「哪些文件是 TU」 +的唯一权威答案。`src/prim/prim.c` 是分发器,按宿主 `#include` 对应平台实现,一条就覆盖三平台。 + +`MI_MALLOC_OVERRIDE` 保持关闭(`alloc-override.c:13` 是它的门)。mimalloc 能接管全局 +malloc/free,但让一个依赖悄悄替换进程分配器不该由包决定。`mi_*` API 不受影响。 + +### 2.2 harfbuzz —— 用上游的 amalgamation + +上游是 meson 构建,在此复刻意味着跟踪 ~137 个 `.cc` 加一份生成的 config。 +`src/harfbuzz.cc` 正是上游为这种场景提供的「只编一个文件」路径,是**上游自己支持的**, +所以 `sources` 只有一行,且不会随 release 演进而脱节。 + +该 amalgamation 同时 `#include` 了本包并未启用的后端(CoreText / DirectWrite / GDI / +GLib / Graphite2 / ICU),每个都在自己的 `HAVE_*` 门后,缺门时编译为空。因此**只声明 +`HAVE_FREETYPE`** 即可精确选中 FreeType 桥接,无需改动任何上游文件。 + +不需要 `config.h`:HarfBuzz 只在 `HAVE_CONFIG_H` 下读它,这里不定义,`hb-config.hh` +提供默认值。这正是它能保持纯源码包、无 configure 步骤的原因。 + +**`HB_NO_MT` 刻意不设。** 它会去掉 HarfBuzz 的原子操作与锁,仅当消费者保证单线程使用时 +才成立 —— 而共享索引包无法替它的消费者作此承诺。 + +### 2.3 msdfgen —— config 不是可选项 + +`core/base.h` 第 7 行就是 `#include `,而该文件由 CMake 从 +`cmake/msdfgen-config.h.in` 生成。**不生成它连 `core/` 都编不了**,这也是本包必须叠加 +形态 E 的原因。 + +选择生成 config 而非传 `-D` 旗标,还有一层更重要的理由:**它让库与消费者天然一致**。 +`base.h` 被每个公开头间接包含,所以这份文件是「SVG / PNG / Skia 哪些存在」的唯一出处。 +包自己编译时的 `cxxflags` 是私有的,消费者 `#include ` 时看不到, +两边就会对「有哪些声明」产生分歧。 + +`ext/` 四个单元只编 `import-font.cpp`,其余三个各需一个本索引没有的库: + +| 单元 | 需要 | +|---|---| +| `ext/resolve-shape-geometry.cpp` | Skia | +| `ext/import-svg.cpp` | TinyXML2 | +| `ext/save-png.cpp` | libpng 或 LodePNG | + +它们的**头文件**仍可经 `msdfgen-ext.h` 到达,但上游给每个都加了宏门,生成的 config +里写上 `MSDFGEN_DISABLE_SVG` / `MSDFGEN_DISABLE_PNG` 后,那些声明根本不存在。 +Skia 不需要宏:它的块是 `#ifdef MSDFGEN_USE_SKIA`,默认即关。 + +**`MSDFGEN_USE_CPP11` 刻意不开。** 它不是构建期开关:它给 `Bitmap` 增加移动构造 +(`core/Bitmap.h:19`),即**改变了一个跨库边界类型的布局与 ABI**。包无法保证每个消费者都 +同样定义它 —— 而直接 `#include ` 的消费者根本看不到任何 shim 里的定义 —— +两边就会静默分歧。关掉它的代价只是少了几次移动,换来单一 ABI。 + +### 2.4 msdfgen 的 `msdfgen/` 前缀 + +上游把 `msdfgen.h` / `msdfgen-ext.h` 放在**仓库根**,但所有打包方式(vcpkg、xmake-repo、 +发行版)都装到 `include/msdfgen/` 下,消费者源码写的也是这个。原地构建拿到的是根布局, +所以 `mcpp_generated/msdfgen/*.h` 把约定拼写补回来。裸 `` 同样可用 —— +wrap 目录本来就在 include 路径上。 + +方向与上游 install 相反:上游写一个全局 `msdfgen.h` 转发到 `msdfgen/msdfgen.h` +(`CMakeLists.txt:285-289`),这里因为在源码树内构建,根部那个才是真的。 + +## 3. CN 镜像 + +三个都在 gitcode `mcpp-res` 下建仓并发 release,上传与 GLOBAL **字节一致**的 tarball。 + +| slug | 版本 | sha256 | +|---|---|---| +| `mimalloc` | 3.4.5 | `19a43af0645c57d348e729d5b31e23e912582911bb1047f795790834d3416221` | +| `harfbuzz` | 14.3.0 | `566e996a1b40486954fb7110ffe6eb88a0f7958bb466cdb023b0302618acea4a` | +| `msdfgen` | 1.13 | `93cd1ad8918c1a78c5c96e82d4f4c77f0eb86c2e7e8579a0967e54196c4b7167` | + +sha256 按 SOP 计算两遍确认稳定。闭环校验(CN 返回 200 且与 GLOBAL 字节一致)三个全过。 + +## 4. feature 评估 + +**三个包都没有实现 feature**,各有其因: + +- **mimalloc**:可选项(override、secure、valgrind、统计档位)全是**编译期 define**, + 而 0.0.68 的 features 表仅能门控 `sources`。按 SKILL 的判定准则,这类不可门控。 +- **harfbuzz**:其余后端(CoreText / DirectWrite / Graphite2 / ICU / GLib)确实是可编译源码, + 但它们已在 amalgamation 内部,无法按源文件切分;开关同样是 define(`HAVE_*`)。 +- **msdfgen**:`ext/` 三个单元**形式上**符合 feature 的定义(额外可编译源码),但每个都需要 + 一个本索引尚无的库(Skia / TinyXML2 / libpng-或-LodePNG),且开关还要同时写进生成的 config + 才能让消费者看见。等这些依赖进索引后可再评估。 + +## 5. 验证 + +测试成员:`tests/examples/{mimalloc,harfbuzz,msdfgen}`,均已登记进根 `mcpp.toml` +的 `[workspace].members`。 + +每个测试都有**可失败的有效断言**,而不是只验证链接成功: + +- **mimalloc** —— `mi_usable_size` 必须 ≥ 请求大小(证明指针来自 mimalloc 自己的簿记 + 而非某个 fallback)、`mi_zalloc` 必须真的清零、`mi_malloc_aligned` 必须对齐、 + 私有 heap 必须 `mi_heap_contains` 自己发出的块。 +- **harfbuzz** —— 真的整形一段文本:`glyph_count` 必须等于输入长度(若 + `hb-ot-shape.cc` 没被编进 amalgamation,这里会是 0),`cluster` 必须跟踪输入偏移; + 再取 `hb_ft_face_create_referenced` 的地址,证明 FreeType 后端**编进来了**而不只是 + 被声明(缺 `-DHAVE_FREETYPE` 时这是链接期错误)。 +- **msdfgen** —— 手工构造一个正方形,生成 MSDF,断言场是**有符号的**(内部 > 0.5、 + 外部 < 0.5,全零位图两条都过不了),且**三通道确有差异**(否则说明 edge coloring + 没跑,"多通道"这件事没发生);最后经 `msdfgen::initializeFreetype` 证明 + `ext/import-font` 在位、传递依赖 `compat.freetype` 到达了链接行。 + 测试用 `msdfgen/` 前缀 include,让生成的 shim 也被 CI 覆盖。 + 缠绕方向交给上游的 `shape.orientContours()` 归一,断言因而不依赖轮廓的书写顺序。 + +冷验证(先删 `target/` 与 `.mcpp/`)结果: + +``` +mimalloc test result ok. 1 passed; 0 failed; finished in 4.32s +harfbuzz test result ok. 1 passed; 0 failed; finished in 31.35s +msdfgen test result ok. 1 passed; 0 failed; finished in 14.63s +``` + +## 6. 过程中踩到的两个坑 + +1. **`generated_files` 的多行内容必须用长括号串**(`[==[ … ]==]`),不能用 `..` 拼接。 + 描述符读取器是轻量 Lua **解析器**而非求值器,会把 `..` 表达式的续行当成新的 key, + 报 `malformed mcpp segment near key 'ifndef'`。既有包(如 `compat.libpng` 的 + `pnglibconf.h`)用的就是长括号串。 +2. **msdfgen 的符号约定不能靠猜**。初版测试按书写顺序假定内外,结果 `inside=-1.375`、 + `outside=2.375` —— 方向恰好相反。正解是调用上游自己的 `shape.orientContours()` + 做归一(它的独立工具对导入几何做的就是这件事),而不是把断言反过来写。 + +## 7. 注意事项 + +- harfbuzz 的 tarball 是 36 MB,是本索引里较大的一个;CI 拉取耗时可见(构建本身很快, + 单 TU)。 +- msdfgen 版本号在 `xpm` 里是裸 `1.13`(上游 tag 是 `v1.13`,`vcpkg.json` 里写 `1.13.0`)。 + 生成的 config 里 `MSDFGEN_VERSION` 取 `1.13.0`,与上游 `version.cmake` 的读取结果一致。 +- mimalloc 的 `MSDFGEN`/`HB` 之外还声明了平台链接库(linux 的 `-lpthread -lrt -latomic`, + windows 的 `psapi/shell32/user32/advapi32/bcrypt`),取自 `CMakeLists.txt:614-626`。 diff --git a/README.md b/README.md index f37547d..56c2f3a 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,8 @@ Two kinds of packages live here: | header-only (with `features`) | [`compat.eigen`](pkgs/c/compat.eigen.lua) | | header-only, nothing to gate | [`compat.CLI11`](pkgs/c/compat.CLI11.lua) (a command line parser whose every definition is `CLI11_INLINE`, so the package is `*/include` plus an anchor TU. Upstream's two extras stay out: `src/Precompile.cpp` only means anything when `CLI11_COMPILE` also reaches the CONSUMER's translation units — an interface define, not a sources-only gate — and `src/modules/CLI11.cppm` is a module layer, which is a package shape of its own rather than a feature of the compat package) | | Runtime loader compat (pure sources, sidestepping upstream codegen/asm) | [`compat.vulkan`](pkgs/c/compat.vulkan.lua) (the Khronos loader: `loader/generated/` is checked in, and the assembly path degrades to plain C through `UNKNOWN_FUNCTIONS_SUPPORTED`, so no CMake/Python/assembler is needed; windows deferred) · [`compat.vulkan-headers`](pkgs/c/compat.vulkan-headers.lua) | -| Whole-source direct build + generated config (only where a platform lacks one) | [`compat.curl`](pkgs/c/compat.curl.lua) (win32 uses upstream's checked-in config, unix generates one) · [`compat.sdl2`](pkgs/c/compat.sdl2.lua) (win/mac use upstream's checked-in config; linux generates one and enables X11 by hand) · [`compat.c-ares`](pkgs/c/compat.c-ares.lua) (91 TUs; the release tarball already ships `ares_build.h` and a Windows config, so only `ares_config.h` is snapshotted per OS) | +| Whole-source direct build + generated config (only where a platform lacks one) | [`compat.curl`](pkgs/c/compat.curl.lua) (win32 uses upstream's checked-in config, unix generates one) · [`compat.sdl2`](pkgs/c/compat.sdl2.lua) (win/mac use upstream's checked-in config; linux generates one and enables X11 by hand) · [`compat.c-ares`](pkgs/c/compat.c-ares.lua) (91 TUs; the release tarball already ships `ares_build.h` and a Windows config, so only `ares_config.h` is snapshotted per OS) · [`compat.msdfgen`](pkgs/c/compat.msdfgen.lua) (msdfgen 1.13 — the config is not optional here: `core/base.h` opens with `#include `, so without generating it nothing compiles, not even `core/`. Generating it rather than passing `-D` flags is also what makes the library and its consumers agree BY CONSTRUCTION — `base.h` is reached from every public header, so the file is the single place that says which of SVG/PNG/Skia exist. Of the four `ext/` units only `import-font.cpp` is built; the other three each need a library this index does not carry, and their declarations disappear through the same generated config. `MSDFGEN_USE_CPP11` is left off on purpose: it adds move constructors to `Bitmap`, so it changes the layout of a type that crosses the library boundary, and a package cannot guarantee every consumer defines it identically) | +| Upstream amalgamation (one TU is the whole library) | [`compat.harfbuzz`](pkgs/c/compat.harfbuzz.lua) (HarfBuzz 14.3.0 — upstream builds with meson, and reproducing that here would mean tracking ~137 `.cc` files plus a generated config. `src/harfbuzz.cc` is upstream's own supported "compile one file" path, so `sources` is a single line that cannot drift out of sync with a release. The amalgamation also `#include`s the CoreText/DirectWrite/GDI/GLib/Graphite2 backends, each behind its own `HAVE_*` gate, so naming only `HAVE_FREETYPE` selects the FreeType bridge and compiles the rest to nothing. `HB_NO_MT` is deliberately NOT set: it removes HarfBuzz's atomics, which is only sound under a single-threading promise a shared package cannot make for its consumers) · [`compat.mimalloc`](pkgs/c/compat.mimalloc.lua) (mimalloc 3.4.5 — the opposite lesson: it also ships an amalgamation (`src/static.c`), and using it would be wrong. A `src/*.c` glob is wrong three ways, each a LINK error rather than a compile error — `static.c` duplicates every symbol, and `free.c`/`alloc-override.c` are `#include`d by `alloc.c` rather than being TUs — so the source list is upstream's own `mi_sources`. `MI_MALLOC_OVERRIDE` stays off: a dependency silently taking over the process allocator is not a package's call) | | Upstream codegen frozen into the mirror archive | [`compat.godot-cpp`](pkgs/c/compat.godot-cpp.lua) (two versions: `4.5.0` = the `godot-4.5-stable` bindings, `10.0.0-rc1` = godot-cpp's own 10.x line, whose bindings target Godot 4.6. The ~1000 GDExtension classes under `gen/` exist in no upstream tag archive — upstream's `binding_generator.py` emits them at build time. Running it once offline and publishing upstream's tree byte-for-byte **plus** `gen/` keeps Python off the consumer side entirely; `tools/godot-cpp/repack.sh` reproduces the archive deterministically and refuses to publish if any upstream file differs) | | Header package filling a gap in the index | [`compat.glx-headers`](pkgs/c/compat.glx-headers.lua) (libglvnd's `GL/glx.h`, absent from the Khronos registry and required by SDL's X11 backend) | | C++ application framework compat (dependencies reuse packages already in the index) | [`compat.eui-neo`](pkgs/e/compat.eui-neo.lua) (upstream's `3rd/` ships 8 vendored dependencies; none of them is compiled here — all are redirected to the same-version `compat.*` packages in this index) | diff --git a/README.zh-CN.md b/README.zh-CN.md index 33436fb..a4a43a1 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -41,7 +41,8 @@ mcpp self config --mirror CN # 切换至国内镜像,默认使用 GLOBAL 上 | header-only(含 `features`) | [`compat.eigen`](pkgs/c/compat.eigen.lua) | | header-only(无可门控组件) | [`compat.CLI11`](pkgs/c/compat.CLI11.lua)(命令行解析器,全部定义都是 `CLI11_INLINE`,故整包就是 `*/include` 加一个 anchor TU。上游两个额外件都不收:`src/Precompile.cpp` 只有在 `CLI11_COMPILE` 同时到达**消费者** TU 时才有意义 —— 那是 interface define,不是 sources 门控;`src/modules/CLI11.cppm` 属于模块层,是另一种包形态,而非 compat 包的 feature) | | 运行时 loader compat(纯源码,绕开上游 codegen/asm) | [`compat.vulkan`](pkgs/c/compat.vulkan.lua)(Khronos loader:`loader/generated/` 已签入,汇编路径经 `UNKNOWN_FUNCTIONS_SUPPORTED` 降级为纯 C,故无需 CMake/Python/汇编器;windows 延后)· [`compat.vulkan-headers`](pkgs/c/compat.vulkan-headers.lua) | -| 全源码直编 + 生成 config(仅缺口平台) | [`compat.curl`](pkgs/c/compat.curl.lua)(win32 用上游签入 config,unix 生成) · [`compat.sdl2`](pkgs/c/compat.sdl2.lua)(win/mac 用上游签入 config,linux 生成 + 手工开 X11) · [`compat.c-ares`](pkgs/c/compat.c-ares.lua)(91 TU;release tarball 已自带 `ares_build.h` 与 Windows 配置,故只需按 OS 冻结 `ares_config.h`) | +| 全源码直编 + 生成 config(仅缺口平台) | [`compat.curl`](pkgs/c/compat.curl.lua)(win32 用上游签入 config,unix 生成) · [`compat.sdl2`](pkgs/c/compat.sdl2.lua)(win/mac 用上游签入 config,linux 生成 + 手工开 X11) · [`compat.c-ares`](pkgs/c/compat.c-ares.lua)(91 TU;release tarball 已自带 `ares_build.h` 与 Windows 配置,故只需按 OS 冻结 `ares_config.h`) · [`compat.msdfgen`](pkgs/c/compat.msdfgen.lua)(msdfgen 1.13 —— 这里的 config 不是可选项:`core/base.h` 开头就是 `#include `,不生成它连 `core/` 都编不了。选择生成它而非传 `-D`,还使库与消费者**天然一致** —— `base.h` 被每个公开头间接包含,于是该文件成为「SVG/PNG/Skia 哪些存在」的唯一出处。`ext/` 四个单元只编 `import-font.cpp`,其余三个各需一个本索引没有的库,其声明经由同一份生成 config 一并消失。`MSDFGEN_USE_CPP11` 刻意不开:它给 `Bitmap` 增加移动构造,即改变了跨库边界类型的布局,而包无法保证每个消费者都同样定义它) | +| 上游 amalgamation(单 TU 即整库) | [`compat.harfbuzz`](pkgs/c/compat.harfbuzz.lua)(HarfBuzz 14.3.0 —— 上游用 meson,在此复刻意味着跟踪 ~137 个 `.cc` 加一份生成的 config。`src/harfbuzz.cc` 正是上游自己支持的「只编一个文件」路径,于是 `sources` 只有一行,且不会与 release 脱节。该 amalgamation 同时 `#include` 了 CoreText/DirectWrite/GDI/GLib/Graphite2 各后端,每个都有自己的 `HAVE_*` 门,因此仅声明 `HAVE_FREETYPE` 即可精确选中 FreeType 桥接,其余编译为空。`HB_NO_MT` 刻意不设:它会去掉 HarfBuzz 的原子操作与锁,仅在消费者保证单线程时才成立,而共享包无法替消费者作此承诺)· [`compat.mimalloc`](pkgs/c/compat.mimalloc.lua)(mimalloc 3.4.5 —— 反向的教训:它同样带 amalgamation(`src/static.c`),但用它是错的。`src/*.c` 通配会在三处出错,且每一处都是**链接期**而非编译期报错 —— `static.c` 会让每个符号重复,`free.c` / `alloc-override.c` 则是被 `alloc.c` `#include` 的、并非独立 TU —— 故源列表取上游自己的 `mi_sources`。`MI_MALLOC_OVERRIDE` 保持关闭:让一个依赖悄悄接管进程分配器,不该由包来决定)| | 上游 codegen 前置冻结进镜像归档 | [`compat.godot-cpp`](pkgs/c/compat.godot-cpp.lua)(两个版本:`4.5.0` 是 `godot-4.5-stable` 的绑定,`10.0.0-rc1` 是 godot-cpp 自己的 10.x 线、对应 Godot 4.6。`gen/` 下约 1000 个 GDExtension 类不在任何上游 tag 归档里,由上游 `binding_generator.py` 在构建时生成。改为离线跑一次,把上游源码树逐字节原样 **加上** `gen/` 一起发布,消费侧就完全不需要 Python;`tools/godot-cpp/repack.sh` 可确定性复现该归档,且上游文件一旦有出入即拒绝打包) | | 补索引空缺的头文件包 | [`compat.glx-headers`](pkgs/c/compat.glx-headers.lua)(libglvnd 的 `GL/glx.h`,Khronos registry 不含,SDL 的 X11 后端必需) | | C++ 应用框架 compat(依赖复用索引内既有包) | [`compat.eui-neo`](pkgs/e/compat.eui-neo.lua)(上游 `3rd/` 自带 8 个 vendored 依赖,此处一个不编,全部改指索引内同版本 `compat.*`) | diff --git a/mcpp.toml b/mcpp.toml index 34c814b..df2c7b4 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -81,6 +81,9 @@ members = [ "tests/examples/tray", "tests/examples/yyjson", "tests/examples/magic_enum", + "tests/examples/mimalloc", + "tests/examples/harfbuzz", + "tests/examples/msdfgen", ] # ── The index redirect, hoisted to the workspace root ─────────────────── diff --git a/pkgs/c/compat.harfbuzz.lua b/pkgs/c/compat.harfbuzz.lua new file mode 100644 index 0000000..32c4e84 --- /dev/null +++ b/pkgs/c/compat.harfbuzz.lua @@ -0,0 +1,85 @@ +-- compat.harfbuzz — HarfBuzz text shaping engine, with the FreeType font backend. +-- +-- Shape A (C-source compat, C++ flavour): the user writes `#include ` +-- and, for the FreeType bridge, `#include `. +-- +-- ONE TRANSLATION UNIT, ON PURPOSE. +-- Upstream builds with meson (or a CMake port), and reproducing either here +-- would mean tracking ~137 .cc files plus their generated config. HarfBuzz +-- ships `src/harfbuzz.cc` for exactly this case: an amalgamation that +-- #includes every implementation file. It is upstream's own supported +-- "just compile one file" path, so the source list stays one line and cannot +-- drift out of sync with a release. +-- +-- The amalgamation also #includes the backends this package does not enable +-- (CoreText, DirectWrite, GDI, GLib, Graphite2, ICU). Each of those is behind +-- its own HAVE_* gate and compiles to nothing when the gate is absent, so +-- naming only HAVE_FREETYPE selects precisely the FreeType backend without +-- editing anything. +-- +-- NO config.h. HarfBuzz reads one only under HAVE_CONFIG_H, which is not +-- defined here; `hb-config.hh` then supplies the defaults. That is what keeps +-- this a source-only package with no configure step. +-- +-- HB_NO_MT IS DELIBERATELY NOT SET. It drops HarfBuzz's atomics and locks, +-- which is only sound when the consumer guarantees single-threaded use -- +-- a promise a shared index package cannot make on its consumers' behalf. +package = { + spec = "1", + namespace = "compat", + name = "harfbuzz", + description = "HarfBuzz — OpenType text shaping engine with the FreeType backend", + licenses = {"MIT"}, + repo = "https://github.com/harfbuzz/harfbuzz", + type = "package", + + xpm = { + linux = { + ["14.3.0"] = { + url = { + GLOBAL = "https://github.com/harfbuzz/harfbuzz/archive/refs/tags/14.3.0.tar.gz", + CN = "https://gitcode.com/mcpp-res/harfbuzz/releases/download/14.3.0/harfbuzz-14.3.0.tar.gz", + }, + sha256 = "566e996a1b40486954fb7110ffe6eb88a0f7958bb466cdb023b0302618acea4a", + }, + }, + macosx = { + ["14.3.0"] = { + url = { + GLOBAL = "https://github.com/harfbuzz/harfbuzz/archive/refs/tags/14.3.0.tar.gz", + CN = "https://gitcode.com/mcpp-res/harfbuzz/releases/download/14.3.0/harfbuzz-14.3.0.tar.gz", + }, + sha256 = "566e996a1b40486954fb7110ffe6eb88a0f7958bb466cdb023b0302618acea4a", + }, + }, + windows = { + ["14.3.0"] = { + url = { + GLOBAL = "https://github.com/harfbuzz/harfbuzz/archive/refs/tags/14.3.0.tar.gz", + CN = "https://gitcode.com/mcpp-res/harfbuzz/releases/download/14.3.0/harfbuzz-14.3.0.tar.gz", + }, + sha256 = "566e996a1b40486954fb7110ffe6eb88a0f7958bb466cdb023b0302618acea4a", + }, + }, + }, + + mcpp = { + language = "c++23", + import_std = false, + include_dirs = { "*/src" }, + sources = { "*/src/harfbuzz.cc" }, + -- HAVE_FREETYPE turns on hb-ft.cc inside the amalgamation, which is + -- what makes more than a set of declarations. The public + -- header is installed either way, so without this the consumer gets a + -- link error rather than a compile error -- worth pinning down here. + cxxflags = { "-DHAVE_FREETYPE=1" }, + targets = { ["harfbuzz"] = { kind = "lib" } }, + deps = { ["compat.freetype"] = "2.13.3" }, + linux = { + ldflags = { "-lm", "-lpthread" }, + }, + macosx = { + ldflags = { "-lm" }, + }, + }, +} diff --git a/pkgs/c/compat.mimalloc.lua b/pkgs/c/compat.mimalloc.lua new file mode 100644 index 0000000..2148976 --- /dev/null +++ b/pkgs/c/compat.mimalloc.lua @@ -0,0 +1,109 @@ +-- compat.mimalloc — Microsoft's general-purpose allocator, built as a static lib. +-- +-- Shape A (C-source compat): the user writes `#include ` and calls +-- mi_malloc / mi_free / mi_heap_* directly. +-- +-- WHY THE SOURCE LIST IS ENUMERATED AND NOT A GLOB. +-- `src/*.c` would be wrong three times over, and each way is a link error +-- rather than a compile error: +-- +-- src/static.c is an amalgamation -- it #includes the whole library +-- into one TU, so globbing it alongside the others +-- duplicates every symbol. +-- src/free.c is #included BY alloc.c (`#include "free.c"`, alloc.c:22), +-- src/alloc-override.c likewise (alloc.c:21). Neither is a standalone TU. +-- +-- The list below is upstream's own `mi_sources` (CMakeLists.txt:75-93), which is +-- the only authoritative answer to "which files are TUs". +-- +-- `src/prim/prim.c` is a dispatcher: it #includes the platform implementation +-- (prim/unix, prim/windows, prim/osx, …) for the host it is compiled on, so one +-- entry covers all three platforms with no per-platform source lists. +-- +-- OVERRIDE IS OFF, which is the right default for a package. mimalloc can +-- replace the global malloc/free, but only when MI_MALLOC_OVERRIDE is defined +-- (alloc-override.c:13) -- and a dependency silently taking over the process +-- allocator is not something an index package should decide for its consumer. +-- The mi_* API is unaffected. +package = { + spec = "1", + namespace = "compat", + name = "mimalloc", + description = "mimalloc — compact general-purpose allocator with excellent performance", + licenses = {"MIT"}, + repo = "https://github.com/microsoft/mimalloc", + type = "package", + + xpm = { + linux = { + ["3.4.5"] = { + url = { + GLOBAL = "https://github.com/microsoft/mimalloc/archive/refs/tags/v3.4.5.tar.gz", + CN = "https://gitcode.com/mcpp-res/mimalloc/releases/download/3.4.5/mimalloc-3.4.5.tar.gz", + }, + sha256 = "19a43af0645c57d348e729d5b31e23e912582911bb1047f795790834d3416221", + }, + }, + macosx = { + ["3.4.5"] = { + url = { + GLOBAL = "https://github.com/microsoft/mimalloc/archive/refs/tags/v3.4.5.tar.gz", + CN = "https://gitcode.com/mcpp-res/mimalloc/releases/download/3.4.5/mimalloc-3.4.5.tar.gz", + }, + sha256 = "19a43af0645c57d348e729d5b31e23e912582911bb1047f795790834d3416221", + }, + }, + windows = { + ["3.4.5"] = { + url = { + GLOBAL = "https://github.com/microsoft/mimalloc/archive/refs/tags/v3.4.5.tar.gz", + CN = "https://gitcode.com/mcpp-res/mimalloc/releases/download/3.4.5/mimalloc-3.4.5.tar.gz", + }, + sha256 = "19a43af0645c57d348e729d5b31e23e912582911bb1047f795790834d3416221", + }, + }, + }, + + mcpp = { + language = "c++23", + import_std = false, + c_standard = "c11", + include_dirs = { "*/include" }, + sources = { + "*/src/alloc.c", + "*/src/alloc-aligned.c", + "*/src/alloc-posix.c", + "*/src/arena.c", + "*/src/arena-meta.c", + "*/src/bitmap.c", + "*/src/heap.c", + "*/src/init.c", + "*/src/libc.c", + "*/src/options.c", + "*/src/os.c", + "*/src/page.c", + "*/src/page-map.c", + "*/src/random.c", + "*/src/stats.c", + "*/src/theap.c", + "*/src/threadlocal.c", + "*/src/prim/prim.c", + }, + targets = { ["mimalloc"] = { kind = "lib" } }, + deps = {}, + linux = { + -- CMakeLists.txt:618-626 -- pthread for thread state, rt for + -- clock_gettime/shm, atomic for the 64-bit atomics on targets + -- where libatomic is not folded into libgcc. + ldflags = { "-lpthread", "-lrt", "-latomic" }, + }, + macosx = { + ldflags = { "-lpthread" }, + }, + windows = { + -- CMakeLists.txt:614 -- psapi/bcrypt for process memory info and + -- the RNG seed, the rest for the Win32 primitives. + ldflags = { "-lpsapi", "-lshell32", "-luser32", "-ladvapi32", "-lbcrypt" }, + }, + }, +} diff --git a/pkgs/c/compat.msdfgen.lua b/pkgs/c/compat.msdfgen.lua new file mode 100644 index 0000000..a77717c --- /dev/null +++ b/pkgs/c/compat.msdfgen.lua @@ -0,0 +1,140 @@ +-- compat.msdfgen — multi-channel signed distance field generator, core + the +-- FreeType font importer. +-- +-- Shape A (C-source compat, C++ flavour) plus generated forwarding headers. +-- The user writes `#include ` and +-- `#include `. +-- +-- WHY THE msdfgen/ PREFIX IS GENERATED. +-- Upstream keeps `msdfgen.h` and `msdfgen-ext.h` at the repository ROOT, but +-- every packaging of msdfgen (vcpkg, xmake-repo, distro packages) installs them +-- under `include/msdfgen/`, and that is what consumers' source actually says. +-- Building in place gives the root layout, so `mcpp_generated/msdfgen/*.h` +-- restores the conventional spelling. The raw `` spelling keeps +-- working too -- the wrap directory is on the include path either way. +-- +-- WHICH SOURCES, AND WHY NOT ALL OF ext/. +-- `core/` is dependency-free and always built. Of the four ext/ units only +-- `import-font.cpp` is here, because the other three each drag in a library +-- that is not in this index: +-- +-- ext/resolve-shape-geometry.cpp Skia +-- ext/import-svg.cpp TinyXML2 +-- ext/save-png.cpp libpng or LodePNG +-- +-- Their HEADERS are still reachable through msdfgen-ext.h -- upstream gates +-- each behind its own macro, so the generated shim sets MSDFGEN_DISABLE_SVG +-- and MSDFGEN_DISABLE_PNG and the declarations simply are not there. Skia +-- needs no macro: its block is `#ifdef MSDFGEN_USE_SKIA`, off by default. +-- +-- MSDFGEN_USE_CPP11 IS DELIBERATELY NOT SET. +-- It is not a build-only switch: it adds move constructors to `Bitmap` +-- (core/Bitmap.h:19), so it changes the layout and ABI of a type that crosses +-- the library boundary. A package cannot guarantee every consumer defines it +-- identically -- and a consumer that includes `` directly would not +-- see a shim's definition at all -- so the two sides would silently disagree. +-- Leaving it off costs some moves and keeps one ABI. +package = { + spec = "1", + namespace = "compat", + name = "msdfgen", + description = "msdfgen — multi-channel signed distance field generator (core + FreeType import)", + licenses = {"MIT"}, + repo = "https://github.com/Chlumsky/msdfgen", + type = "package", + + xpm = { + linux = { + ["1.13"] = { + url = { + GLOBAL = "https://github.com/Chlumsky/msdfgen/archive/refs/tags/v1.13.tar.gz", + CN = "https://gitcode.com/mcpp-res/msdfgen/releases/download/1.13/msdfgen-1.13.tar.gz", + }, + sha256 = "93cd1ad8918c1a78c5c96e82d4f4c77f0eb86c2e7e8579a0967e54196c4b7167", + }, + }, + macosx = { + ["1.13"] = { + url = { + GLOBAL = "https://github.com/Chlumsky/msdfgen/archive/refs/tags/v1.13.tar.gz", + CN = "https://gitcode.com/mcpp-res/msdfgen/releases/download/1.13/msdfgen-1.13.tar.gz", + }, + sha256 = "93cd1ad8918c1a78c5c96e82d4f4c77f0eb86c2e7e8579a0967e54196c4b7167", + }, + }, + windows = { + ["1.13"] = { + url = { + GLOBAL = "https://github.com/Chlumsky/msdfgen/archive/refs/tags/v1.13.tar.gz", + CN = "https://gitcode.com/mcpp-res/msdfgen/releases/download/1.13/msdfgen-1.13.tar.gz", + }, + sha256 = "93cd1ad8918c1a78c5c96e82d4f4c77f0eb86c2e7e8579a0967e54196c4b7167", + }, + }, + }, + + mcpp = { + language = "c++23", + import_std = false, + include_dirs = { "*", "mcpp_generated" }, + -- Shape E as well as A: msdfgen reaches its OWN config header through + -- the same prefix (`core/base.h:7` does + -- `#include `), and CMake generates that file + -- from `cmake/msdfgen-config.h.in`. Without it nothing compiles at all, + -- not even core/. + -- + -- Generating it here rather than passing -D flags is what makes the + -- library and its consumers agree BY CONSTRUCTION: base.h is reached + -- from every public header, so whatever this file says is what both + -- sides see. Flags on the package's own compile line could not do + -- that -- they are private to this build, and a consumer including + -- would disagree about which declarations + -- exist. + -- + -- Values follow CMakeLists.txt:233-265 for a static build with + -- MSDFGEN_USE_SKIA=OFF, MSDFGEN_DISABLE_SVG=ON, MSDFGEN_DISABLE_PNG=ON, + -- MSDFGEN_USE_CPP11=OFF; version from vcpkg.json (1.13.0). + -- + -- Long-bracket strings, not `..` concatenation: the descriptor reader + -- is a lightweight Lua parser, not an evaluator, and reads a `..` + -- expression's continuation lines as new keys + -- ("malformed mcpp segment near key 'ifndef'"). + generated_files = { + ["mcpp_generated/msdfgen/msdfgen-config.h"] = [==[ +#pragma once + +#define MSDFGEN_PUBLIC +#define MSDFGEN_EXT_PUBLIC + +#define MSDFGEN_VERSION 1.13.0 +#define MSDFGEN_VERSION_MAJOR 1 +#define MSDFGEN_VERSION_MINOR 13 +#define MSDFGEN_VERSION_REVISION 0 +#define MSDFGEN_COPYRIGHT_YEAR 2025 + +#define MSDFGEN_EXTENSIONS +#define MSDFGEN_DISABLE_SVG +#define MSDFGEN_DISABLE_PNG +]==], + -- The conventional install spelling. Upstream's own install does + -- exactly this (CMakeLists.txt:285-289 writes a global msdfgen.h + -- that forwards to msdfgen/msdfgen.h); here the direction is + -- reversed because the build happens in the source tree, where the + -- root header is the real one. + ["mcpp_generated/msdfgen/msdfgen.h"] = [==[ +#pragma once +#include +]==], + ["mcpp_generated/msdfgen/msdfgen-ext.h"] = [==[ +#pragma once +#include +]==], + }, + sources = { + "*/core/*.cpp", + "*/ext/import-font.cpp", + }, + targets = { ["msdfgen"] = { kind = "lib" } }, + deps = { ["compat.freetype"] = "2.13.3" }, + }, +} diff --git a/tests/examples/harfbuzz/mcpp.toml b/tests/examples/harfbuzz/mcpp.toml new file mode 100644 index 0000000..3430c3b --- /dev/null +++ b/tests/examples/harfbuzz/mcpp.toml @@ -0,0 +1,13 @@ +# HarfBuzz test project: shape a real buffer and assert the FreeType bridge is +# linked in, under `mcpp test`. +# +# compat.freetype arrives transitively through compat.harfbuzz's own deps, so +# it is not repeated here -- the test includes , whose FT_* parameter +# types come from that transitive include path. If the propagation ever broke, +# this member would fail to compile, which is the point. +[package] +name = "harfbuzz-tests" +version = "0.1.0" + +[dependencies.compat] +harfbuzz = "14.3.0" diff --git a/tests/examples/harfbuzz/tests/shape.cpp b/tests/examples/harfbuzz/tests/shape.cpp new file mode 100644 index 0000000..13f2669 --- /dev/null +++ b/tests/examples/harfbuzz/tests/shape.cpp @@ -0,0 +1,67 @@ +// compat.harfbuzz — assert the shaper actually runs, and that the FreeType +// backend is compiled in rather than merely declared. +// +// Shaping needs no font file: hb_font_get_empty() gives a font whose glyphs +// are all .notdef, which is enough to prove the buffer pipeline (itemize → +// shape → read back infos/positions) is wired and returns one glyph per input +// codepoint. That is a real assertion -- if hb-ot-shape.cc had not been +// compiled into the amalgamation, glyph_count would be 0. +#include +#include +import std; + +int main() { + bool ok = true; + + auto check = [&](bool cond, std::string_view what) { + if (!cond) { + std::println("FAIL: {}", what); + ok = false; + } + }; + + check(hb_version_string() != nullptr, "hb_version_string returned null"); + + unsigned major = 0, minor = 0, micro = 0; + hb_version(&major, &minor, µ); + check(major >= 14, "unexpected HarfBuzz major version"); + + // Shape a short ASCII run through the empty font. + hb_buffer_t* buf = hb_buffer_create(); + check(hb_buffer_allocation_successful(buf), "hb_buffer_create failed"); + + static constexpr std::string_view text = "mcpp"; + hb_buffer_add_utf8(buf, text.data(), static_cast(text.size()), 0, + static_cast(text.size())); + hb_buffer_set_direction(buf, HB_DIRECTION_LTR); + hb_buffer_set_script(buf, HB_SCRIPT_LATIN); + hb_buffer_set_language(buf, hb_language_from_string("en", -1)); + + hb_shape(hb_font_get_empty(), buf, nullptr, 0); + + unsigned glyph_count = 0; + hb_glyph_info_t* infos = hb_buffer_get_glyph_infos(buf, &glyph_count); + hb_glyph_position_t* pos = hb_buffer_get_glyph_positions(buf, &glyph_count); + + check(glyph_count == text.size(), "shaped glyph count != input length"); + check(infos != nullptr && pos != nullptr, "shaped buffer has no infos/positions"); + if (infos != nullptr) { + // The empty font maps everything to .notdef (glyph 0); cluster values + // must still track the original byte offsets. + for (unsigned i = 0; i < glyph_count; ++i) { + check(infos[i].cluster == i, "cluster does not track input offset"); + } + } + + hb_buffer_destroy(buf); + + // The FreeType bridge: taking its address proves hb-ft.cc was compiled in. + // Without -DHAVE_FREETYPE the header still declares this and the build + // fails at LINK time, which is exactly what this catches. + check(reinterpret_cast(&hb_ft_face_create_referenced) != nullptr, + "hb_ft_face_create_referenced missing (FreeType backend not built)"); + + std::println("compat.harfbuzz smoke test: {} (hb {})", ok ? "ok" : "FAILED", + hb_version_string()); + return ok ? 0 : 1; +} diff --git a/tests/examples/mimalloc/mcpp.toml b/tests/examples/mimalloc/mcpp.toml new file mode 100644 index 0000000..7eee8f2 --- /dev/null +++ b/tests/examples/mimalloc/mcpp.toml @@ -0,0 +1,14 @@ +# mimalloc test project: allocate through the mi_* API and assert the allocator +# actually owns what it handed back, under `mcpp test`. +# +# Resolves compat.mimalloc from this repo's own index via the inherited +# workspace-root `[indices]`. The dependency is spelled QUALIFIED for the same +# reason every other member does it: the redirect is keyed by the REQUEST's +# namespace, so a bare `mimalloc = "3.4.5"` would resolve from the published +# remote index and quietly stop testing this checkout. +[package] +name = "mimalloc-tests" +version = "0.1.0" + +[dependencies.compat] +mimalloc = "3.4.5" diff --git a/tests/examples/mimalloc/tests/alloc.cpp b/tests/examples/mimalloc/tests/alloc.cpp new file mode 100644 index 0000000..b0e8d02 --- /dev/null +++ b/tests/examples/mimalloc/tests/alloc.cpp @@ -0,0 +1,62 @@ +// compat.mimalloc — assert the allocator is real, not just that it links. +// +// Every check below can fail: mi_usable_size reports what mimalloc actually +// reserved (so it proves the pointer came from mimalloc's own bookkeeping and +// not from a fallback), mi_zalloc must return zeroed memory, and a private +// heap must be able to hand out and destroy blocks independently of the +// default heap. +#include +import std; + +int main() { + bool ok = true; + + auto check = [&](bool cond, std::string_view what) { + if (!cond) { + std::println("FAIL: {}", what); + ok = false; + } + }; + + // 1. Basic allocation, and mimalloc knows the block's real size. + void* p = mi_malloc(1024); + check(p != nullptr, "mi_malloc(1024) returned null"); + if (p) { + // mimalloc rounds up to a size class, so usable >= requested. + check(mi_usable_size(p) >= 1024, "mi_usable_size < requested size"); + std::memset(p, 0xAB, 1024); + check(static_cast(p)[1023] == 0xAB, "written byte did not survive"); + mi_free(p); + } + + // 2. mi_zalloc must actually zero. + constexpr std::size_t n = 512; + auto* z = static_cast(mi_zalloc(n)); + check(z != nullptr, "mi_zalloc returned null"); + if (z) { + check(std::all_of(z, z + n, [](unsigned char c) { return c == 0; }), + "mi_zalloc did not zero the block"); + mi_free(z); + } + + // 3. Aligned allocation honours the alignment. + void* a = mi_malloc_aligned(256, 64); + check(a != nullptr, "mi_malloc_aligned returned null"); + if (a) { + check(reinterpret_cast(a) % 64 == 0, "alignment not honoured"); + mi_free(a); + } + + // 4. A private heap allocates and tears down independently. + mi_heap_t* heap = mi_heap_new(); + check(heap != nullptr, "mi_heap_new returned null"); + if (heap) { + void* hp = mi_heap_malloc(heap, 4096); + check(hp != nullptr, "mi_heap_malloc returned null"); + check(mi_heap_contains(heap, hp), "heap does not contain its own block"); + mi_heap_destroy(heap); // frees hp with it + } + + std::println("compat.mimalloc smoke test: {}", ok ? "ok" : "FAILED"); + return ok ? 0 : 1; +} diff --git a/tests/examples/msdfgen/mcpp.toml b/tests/examples/msdfgen/mcpp.toml new file mode 100644 index 0000000..684b60e --- /dev/null +++ b/tests/examples/msdfgen/mcpp.toml @@ -0,0 +1,11 @@ +# msdfgen test project: build a real shape, generate an MSDF from it, and +# assert the field is a distance field rather than a blank bitmap. +# +# compat.freetype arrives transitively through compat.msdfgen (ext/import-font +# needs it), so it is not repeated here. +[package] +name = "msdfgen-tests" +version = "0.1.0" + +[dependencies.compat] +msdfgen = "1.13" diff --git a/tests/examples/msdfgen/tests/generate.cpp b/tests/examples/msdfgen/tests/generate.cpp new file mode 100644 index 0000000..febfb34 --- /dev/null +++ b/tests/examples/msdfgen/tests/generate.cpp @@ -0,0 +1,94 @@ +// compat.msdfgen — generate a real multi-channel signed distance field and +// assert its contents, not just that the library links. +// +// The shape is a square built by hand, so the test needs no font file and no +// ext/ importer. What makes the assertions real: +// * the field must be signed -- inside the square the distance is positive, +// outside it is negative. A blank or all-zero bitmap fails both. +// * edge colouring must have run, otherwise the three channels are identical +// and the "multi-channel" part of MSDF did nothing. +// +// The `msdfgen/` include prefix is the one this package generates; using it +// here is what keeps the shim covered by CI. +#include +#include +import std; + +int main() { + bool ok = true; + + auto check = [&](bool cond, std::string_view what) { + if (!cond) { + std::println("FAIL: {}", what); + ok = false; + } + }; + + // A 16x16 axis-aligned square, inset from the 32x32 bitmap so that both + // the inside and the outside are sampled. + msdfgen::Shape shape; + msdfgen::Contour& contour = shape.addContour(); + const msdfgen::Point2 p0(8, 8), p1(24, 8), p2(24, 24), p3(8, 24); + contour.addEdge(msdfgen::EdgeHolder(p0, p1)); + contour.addEdge(msdfgen::EdgeHolder(p1, p2)); + contour.addEdge(msdfgen::EdgeHolder(p2, p3)); + contour.addEdge(msdfgen::EdgeHolder(p3, p0)); + + shape.normalize(); + check(shape.validate(), "hand-built shape did not validate"); + + // Winding decides the sign of the field, and the order above is not + // guaranteed to be the one msdfgen calls "inside". orientContours() is + // upstream's own normalizer for exactly this (its standalone tool runs it + // on imported geometry), so the assertions below can talk about inside and + // outside without depending on how the contour happened to be written. + shape.orientContours(); + + msdfgen::edgeColoringSimple(shape, 3.0); + + msdfgen::Bitmap msdf(32, 32); + msdfgen::SDFTransformation t( + msdfgen::Projection(msdfgen::Vector2(1.0), msdfgen::Vector2(0.0)), + msdfgen::Range(4.0)); + msdfgen::generateMSDF(msdf, shape, t); + + // Median of the three channels is the reconstructed signed distance. + auto median_at = [&](int x, int y) { + const float* px = msdf(x, y); + return msdfgen::median(px[0], px[1], px[2]); + }; + + // Centre of the square is inside → positive; a corner of the bitmap is + // far outside → negative. msdfgen encodes 0.5 as the edge. + const float inside = median_at(16, 16); + const float outside = median_at(0, 0); + check(inside > 0.5f, std::format("centre should read inside, got {}", inside)); + check(outside < 0.5f, std::format("corner should read outside, got {}", outside)); + + // Multi-channel: at least one pixel must have channels that differ, + // otherwise edge colouring never ran and this is a plain SDF. + bool multi_channel = false; + for (int y = 0; y < 32 && !multi_channel; ++y) { + for (int x = 0; x < 32; ++x) { + const float* px = msdf(x, y); + if (px[0] != px[1] || px[1] != px[2]) { + multi_channel = true; + break; + } + } + } + check(multi_channel, "all three channels identical — edge colouring did not run"); + + // ext/import-font is compiled in: initializing FreeType through msdfgen + // proves the FreeType-backed half of the package is present, and that the + // transitive compat.freetype dependency reached the link line. + msdfgen::FreetypeHandle* ft = msdfgen::initializeFreetype(); + check(ft != nullptr, "msdfgen::initializeFreetype returned null"); + if (ft) { + msdfgen::deinitializeFreetype(ft); + } + + std::println("compat.msdfgen smoke test: {} (inside={:.3f} outside={:.3f})", + ok ? "ok" : "FAILED", inside, outside); + return ok ? 0 : 1; +}