图形离屏示例,以及两处只在 Linux 之外成立的引擎缺陷 - #586
Merged
Merged
Conversation
图形阶段在 rules-spirv 里被认领了十四个,而全树 .vert/.frag 零个文件 —— 声明覆盖、执行为零。 主示例是离屏渲染而不是窗口,理由是判据:swapchain 需要 surface,无头机器上没有, 于是围绕它写的示例在 CI 里只能被**构建** —— 而「构建通过」对图形管线几乎零信息量, 片元着色器忽略输入、顶点阶段从未跑过、image 从未被渲染进去,三者都能编译并链接。 渲染进 image 再读回像素让结果可断言,走的是窗口应用同一条管线。 判据:四角是清除色(不设容差);中心在三角形内,**每个通道都必须非零** —— 常量色 的片元着色器会给出一个 255 与两个 0,而顶点阶段没跑会留下清除色,这一条把两者与 插值结果分开,且不依赖光栅化的取整。 两条腿实测同为 (124, 70, 62, 255):llvmpipe 与软件光栅器给出同样的字节。像素测试 是契约,CPU 腿证明这个契约不需要 GPU 也能满足,而 device_name() 是唯一区分它们的 东西。 --no-accel 一个字节都不装:Vulkan loader 与软件设备都门控在加速器上。
这两条是把规则包在 macOS 与 Windows 上真正跑一遍时暴露的。两处的共同形状是:
一段代码的正确性依赖于宿主,而这个仓库的 CI 只在其中一个宿主上执行它。
## 一、构建程序的链接不带工具链自己的运行时目录(macOS)
`host_link_tokens` 有两个出口:一个把 clang 的配置逐条写出来,一个信任载荷自带的
`<driver>.cfg`。第二个提前 return,跳过了给 `Toolchain::linkRuntimeDirs` 发
`-L` 与 `-rpath` 的那一段。
信任 cfg 决定的是**链哪些运行时**,它从来没有决定**去哪里找**。macOS 上 `-lc++`
于是经 SDK 解析到系统那份,而头文件来自载荷。两者在头文件引用了系统库还没导出的
符号那一刻分道扬镳 —— macos-14 上编译一个只有 `import std` 的构建程序:
ld64.lld: error: undefined symbol: std::__1::__is_posix_terminal(__sFILE*)
>>> referenced by std::__1::__print::__is_terminal(__sFILE*)
`std::print` 不是 header-only 的,它的两个重载都要到 libc++ **dylib** 里取支持
符号,而那两个符号是在 macOS 14 不带的那一版里加进去的。macOS 15 有,所以这个项目
用到的每一台 macOS runner 都是绿的。
载荷自带 `lib/libc++.1.0.dylib` —— 与它自己的头文件配套的那一份 —— 而
`linkRuntimeDirs` 早就命名了那个目录。给它发 `-L` 与 `-rpath` 是 Linux 那条路
一直在做的事;macOS 上被跳过,只是因为这个分支先 return 了。
⭐ **构建程序正是绝对 rpath 该在的地方**:它从不被分发,mcpp 编译它、在这台机器上
运行它、按这套工具链的身份缓存它。同一个问题对**产物**的答案由分发契约决定,不归
这个函数回答。
判据 `HostFlags.EveryExitNamesTheToolchainRuntimeDirs`:把两个出口都测一遍。为此
`CfgBypass` 补了第三个取值 `Never` —— 缺陷所在的那个分支在 Linux 上不可达
(`LinuxOnly` 在那里折叠成 `Always`),而一条只能在它写给的两个宿主上跑的判据,会
继承同一个盲点。实测:把修复拿掉,这条当场变红。
## 二、版本约束里的 `>` 被 cmd.exe 读成重定向(Windows)
mcpp 把供给请求作为 JSON 参数放在 shell 命令行上交给 xlings。`shell::quote` 回答
的是**子进程**那一层的解析 —— MSVCRT,它给内嵌引号的转义是 `\"` —— 而 cmd.exe
不认这个转义:对它来说每个 `"` 只是在切换引用状态。于是一段 JSON 走到 `>` 的时候,
它背后的引号数是**偶数**,`>` 成了重定向,重定向目标是 JSON 的其余部分:
Provisioning [xlings.workspace] entries declared by dependencies (xim:shaderc@>=2026.3)
The filename, directory name, or volume label syntax is incorrect.
error: provisioning ... failed: xlings exited 1
windows-2022 实测。`>=` 正是每个规则包声明下界用的形态,而**在此之前没有任何一条能
在 Windows 上生效的声明带过 `>`**,所以整个形态在那个宿主上从没被走到过。
修法是标准的双重转义:先按子进程的规则引用,再给每个 cmd 元字符(包括引号本身)前
缀 `^`。没有任何 `"` 是裸的,cmd 就从不进入引用状态,于是每个元字符都是被转义而不是
被引用的 —— 这是两套规则同时成立的唯一状态。cmd 去掉那些 `^`,子进程看到的正是
`quote_windows` 的输出。
⚠️ `%` 不转义也无法转义:变量展开发生在 `^` 处理之前,而 `%%` 只在批处理文件里可用。
这一点写在函数注释里;这里的 JSON 参数不含 `%`。
判据是两个模拟器 —— 一个复现 cmd 的解析,一个复现 MSVCRT 的 argv 解析 —— 断言的是
**子进程收到的参数等于本来要传的那个 JSON**,而不是转义的拼写。另有一条反向判据
断言旧的引用方式确实让 cmd 看见了一个活的元字符,否则正向那条什么都不证明。
端到端的判据在 mcpp-plugins:它的 Windows job 目前用精确版本绕开这条缺陷,等这个
修复发布后把声明改回 `>=2026.3`,那条 job 就是这个修复的端到端判据。
docs/20 新增一节「Which platforms each lane reaches」(中英双份):三件事同时为真 才叫「一条 lane 在某个平台上成立」—— 设备编译器发布了、产物要的运行时够得到、以及 规则自己那段按宿主分岔的代码在那里编译得过。第三件是最容易被默认成立的那一件,而 mcpp:plugins 现在为矩阵里每个平台编译每一条规则,正是它把三处潜伏的宿主差异变成了 对应 runner 上的编译错误。 同一节写明「一条 lane 到得了的平台,它到达的方式是同一个」:规则按宿主区别对待的 全部内容就是四处,每一处都是宿主的性质不是设备的。以及运行时适配层 (compat:*-runtime)是 Linux 的构造 —— 它们不该被「补到三平台」,在另外两个平台上 按构造就不存在。 「尚未实现」补两条按平台分的缺口(HIP 上 Windows、CUDA 13.x 上 Windows),都写明 它们是打包工作而不是引擎工作,以及各自缺的那一块具体是什么。 lane 表的载荷列改成按平台写:rules-spirv 的编译器与 rules-sycl 的三个「把宿主库挡 在外面」的载荷都不再是三平台通用的。 docs/01 加上 10-graphics 两行。 设计文档补 §10 实施回填:§9 三条自我 review 的读数(第一条比预期强、第二条被推翻但 缺陷是真的、第三条成立且是最贵的一条)、「上游有产物 ≠ 重打包就能用」的四处、以及 三处只在 Linux 之外成立的缺陷。
这个示例最初把 Vulkan loader、运行时适配器与软件设备三条都门控在
`cfg(accelerator = "vulkan")` 上。**这条做不到,而且它失败的方式是安静的一半**:
`accelerator` 是从依赖图里解出来的,所以一个由它选出的依赖会决定它自己正在问的那个
答案 —— mcpp 忽略这个谓词并给出警告,而同一个谓词下的 `[build]` 源照常生效。于是包
被丢掉、包含它的源被留下:
src/vulkan/render.cpp:20:10: fatal error: vulkan/vulkan.h: No such file or directory
谓词改成按平台写(只有 Linux 有的两项用 `cfg(linux)`),包无条件,accelerator 只选
`[build]` 源 —— 既有的 examples/09-heterogeneous/vulkan 一直是这个形状。示例 README
把这条写成正文,因为它是使用者会撞上的规则。
反向腿加强:实测两条腿的中心像素逐字节相同(`(124, 70, 62, 255)`),所以 CI 从
「CPU 腿跑起来了」改成「两条腿报出同一个像素、不同的设备名」—— 这才是这个示例声称
的那个契约。
本地实测:lavapipe 腿 `device: llvmpipe (LLVM 22.1.8, 256 bits)`,CPU 腿
`device: cpu rasteriser`,两者 `centre pixel: (124, 70, 62, 255)`。
此前 `examples/` 只在 Linux 上被构建过(`build_examples.sh` 只在那里跑)。这正是 这一轮要消除的形状:一条 lane 里为某个宿主写的那半段,恰好是那个宿主从不走的那半段。 两个 runner 都没有 Vulkan 设备,所以断言的是这个平台决定的那部分:**这个平台用的那个 着色器编译器**(规则在这里声明 `xim:shaderc`,Linux 上是 `xim:glslang`)产出两个 SPIR-V 头,而 Vulkan 那一半编译并链接得上 loader 包。跑起来是 Linux job 的判据 —— 那里有已发布的软件设备,而且两条腿的像素被逐字节比较。 工具链显式命名而不是继承:让它取决于隔壁某一步恰好选了什么,会让这一步的被测对象 取决于步骤顺序,而那不是任何人会去读的性质。
上一条提交让 host_link_tokens 的「信任 cfg」出口也发 `-L<载荷>/lib`,理由是 macOS 上 `-lc++` 会解析到系统那份而头文件来自载荷。**动机是真的,修法是错的。** 那样做会让 `-lc++` 找到工具链自己的 dylib,也就是 `dist::mechanism_for` 在 Mach-O 上明确拒绝的 ToolchainCoupled:LLVM 的 macOS libc++abi 与 libunwind dylib **向上 链接** `/usr/lib/libc++`,系统 libc++ 与工具链的那份同时载入,跨两份释放的对象在 libmalloc 里 abort(#202)。CI 报的正是这条路的第一步 —— 图形示例的构建程序在 macOS 15 上链接停在 `__cxa_end_catch`、`std::runtime_error::~runtime_error()` 与 其余那些系统 libc++ 会再导出、载荷那份不会的 ABI 符号上。 **它被推翻的方式值得记**:本地全部单测绿,而判据是我自己写的那条正向断言;红在一个 我没想到会受影响的对象上,在一个我没在本地跑过的宿主上。 改回去,把这条不对称与它的理由写进代码,判据改成陈述这条决定 (`OnlyTheSpelledOutExitNamesTheToolchainRuntimeDirs`,用 `CfgBypass::Never` 让 那条在 Linux 上不可达的分支可测)。构建程序的缓存键随之回到 default-v2 —— 行为与 这两条提交之前完全一致。 限制照实写下来:**macOS 14 上构建程序不能用 `std::print` / `std::println`**,而 `std::format` 是 header-only 的、没有这个问题。本轮 mcpp:plugins 的六个模块因此全部 改用它,那一半是真的修好了。
macOS 与 Windows 上图形示例的**构建都成功了**:规则供给了 `xim:shaderc@2026.3`、两个
着色器阶段都编译了、Vulkan 那一半也链接上了 loader 包。红的是我写的那条断言。
glslang 写出完整的 `const uint32_t ...[] = {...}`(magic 在 `.h` 里),glslc 写出初始
化列表、由规则在外面补声明(magic 在 `.inc` 里)。**一条只点名 `.h` 的断言,是一条
关于某一个编译器的断言** —— 正是这个 job 存在的理由所要抓的那种形状,只不过这次它抓到
的是自己。
顺带一个陷阱:改成 `grep -qs '<pat>' a.h a.inc` 是错的。**GNU grep 在被点名的文件不
存在时返回 2,即使它在前一个文件里匹配到了,也即使加了 `-s`** —— `-s` 压的是消息不是
状态。于是这条判据在「只产出 header」的那条路上永远失败,而失败原因与被测性质无关。
逐个文件测。
两条腿都实测过:glslang 路(只有 .h)通过;把 magic 改掉当场变红。
Windows e2e 两红,Linux examples 一红,两条都是这一轮自己引入的。
## 一、把引号也一起转义,打断了每一次包获取
上一条提交的做法是:先按子进程的规则引用,再给**每个 cmd 元字符,包括引号本身**
前缀 `^`。理由在纸面上成立 —— 没有裸引号,cmd 就从不进入引用区,于是每个元字符都是
被转义而不是被引用的。
**Windows CI 的读数是:每一次 `install_packages` 都退 1**,包括那两次 JSON 里一个
元字符都没有的(`compat:widget@1.38.1`、`mcpplibs:tpl-demo@1.0.0`)。而这个 job 里
只有那两次调用,也就是**走到这条路的每一次都失败了**。
改成保守的那条规则:引用之后,**按 cmd 看到的引用状态**走一遍(每个 `"` 都翻转它,
因为 cmd 不认 MSVCRT 的 `\"`),只给**落在引用区之外**的元字符加 `^`。引用区里的元
字符本来就是惰性的,而 `^` 在那里是个普通字符。
于是「没有东西要转义」的载荷输出与 `quote_windows` **逐字节相同** —— 那是绝大多数
载荷,也正是被打断的那些。而 `>=2026.3` 里那个 `>` 只多一个 `^`。
判据补了一条:`NothingToEscapeMeansByteIdenticalToPlainQuoting`,直接拿 CI 里失败的
那两个 JSON 当输入。cmd 的模拟器也补上了「`^` 在引用区内是普通字符」这一半 —— 只建
模前一半,它会接受一个 cmd 并不接受的形状。
## 二、示例:`std::size_t` 要 `#include <cstddef>`
`src/cpu/render.cpp` 在 libstdc++ 下编得过、在 libc++ 下编不过:
src/cpu/render.cpp:56:52: error: no type named 'size_t' in namespace 'std'
标准头有权带进它需要的其他头,而带进哪些因实现而异。本地默认工具链是 gcc,CI 那一步
用的是 llvm —— **同一台机器上的两个答案**。点名一个类型的翻译单元必须包含声明它的那个
头,不管上一个实现顺手给了什么。
`src/vulkan/render.cpp` 同样补上(它此前靠 `<vulkan/vulkan.h>` 间接得到)。
可迁移的:**本地验示例要用 CI 那一步用的工具链**(`mcpp build --toolchain llvm@…`),
否则验的是另一个标准库。
平台表里一行写「是」,读起来很容易被当成「跑通了」。三个平台上真的**跑过**的只有 rules-spirv;Windows 上 CUDA 与 SYCL 那条 lane 是**装上并编译过** —— 组件装得上、注册 出程序、规则为那个宿主编译过,但还没有任何一台 runner 在那里端到端驱动过 nvcc 或 dpcpp。 一份规范文档里,这个差别不该留给读者去推。中英双份。
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
一句话:引擎与规则层早就与平台无关,而生态只在 Linux 上完整;把规则包在另外两个
平台上真跑一遍,暴露了三处缺陷 —— 一处已修、一处我的修复被实测推翻、一处是使用者
会撞上的规则。
设计文档:
.agents/docs/2026-09-07-heterogeneous-cross-platform-ecosystem.md(含 §10实施回填)。
一、
examples/10-graphics/offscreen:一条真实的图形管线,判据是像素此前所有异构示例都是计算。这一个是图形:顶点与片段两个着色器阶段、一条 render
pass、
vkCmdDraw画一个三角形、vkCmdCopyImageToBuffer取回像素。离屏,而不是开窗,因为那是可断言的形态:无窗口、无交换链、无表面扩展,在没有显示器
的 runner 上跑得起来,而结果是一段可以逐字节检查的缓冲区。程序自己断言四角等于清除色、
中心不等于清除色、三个通道都非零、alpha 为 255。
同一道接缝背后是一个自己写的软件光栅器。实测两条腿的中心像素逐字节相同
(
(124, 70, 62, 255)),所以 CI 的反向腿断言的是「两条腿报出同一个像素、不同的设备名」—— 图像是契约,设备名是唯一区分它们的东西。
CI 还在 macOS 与 Windows 上各构建一次(不跑,那两台没有 Vulkan 设备):断言的是这个
平台用的那个着色器编译器产出两个 SPIR-V 头、Vulkan 那一半编译并链接得上 loader 包。
一条使用者会撞上的规则:依赖不能被 layer 条件化
cfg(accelerator = ...)下的[build]源生效而依赖被忽略,于是包被丢掉、包含它的源被留下,构建死在
vulkan/vulkan.h: No such file or directory。示例 README 把这一条写成了正文。
二、版本约束里的
>被 cmd.exe 读成重定向(Windows)—— 已修shell::quote回答的是子进程的 argv 解析,cmd 不认那个转义,于是走到>时引号数是偶数。
>=正是每个规则包声明下界用的形态,而在此之前没有任何一条能在 Windows 上生效的声明带过
>。修法是标准双重转义。判据是两个解析器的模拟器,断言子进程收到的参数等于本来要传的那个
JSON,外加一条反向腿断言旧写法确实让 cmd 看见了一个活的元字符。端到端判据在
mcpp-plugins:它今天用精确版本绕开,这个修复发布后改回
>=,那条 Windows job 就是端到端判据。
三、macOS 14 上构建程序用不了
std::println—— 记录,未修,而且是因为修法更糟std::print不是 header-only 的:它的支持符号在 libc++ dylib 里,而 macOS 14 那一版没有。直觉的修法是让
host_link_tokens的「信任 cfg」出口也发-L<载荷>/lib。试过了,CI 推翻了它。 那会让
-lc++解析到工具链自己的 dylib,也就是dist::mechanism_for在 Mach-O 上明确拒绝的 ToolchainCoupled —— LLVM 的 macOSlibc++abi 与 libunwind dylib 向上链接
/usr/lib/libc++,系统 libc++ 与工具链的那份同时载入,跨两份释放的对象在 libmalloc 里 abort(#202)。CI 报的正是这条路的第一步:
链接停在
__cxa_end_catch与其余那些系统 libc++ 会再导出、载荷那份不会的 ABI 符号上。所以改回去,把这条不对称与它的理由写进代码,判据从「断言修复」改成陈述这条决定
(
OnlyTheSpelledOutExitNamesTheToolchainRuntimeDirs;为此给策略枚举补了CfgBypass::Never,让那条在 Linux 上不可达的分支可测)。限制照实写:macOS 14 上构建程序不能用
std::print/std::println。std::format是 header-only 的,没有这个问题 —— 真正修好的那一半在 mcpp:plugins,六个规则模块全部改用它。
四、文档
docs/20新增「Which platforms each lane reaches」(中英双份):三件事同时为真才叫一条lane 在某个平台上成立,而第三件(规则自己那段按宿主分岔的代码编译得过)是最容易被默认
成立的那一件。同节写明四处按宿主的差异、以及运行时适配层是 Linux 的构造 —— 它们不该被
「补到三平台」。「尚未实现」补两条按平台分的缺口,都写明缺的具体是什么。
生态侧(已合入)
shaderc补 macOS arm64 与 Windows x86_64dpcpp补 windows 段;共享 install/config 形状里三处按宿主分的支;一条量了很久却对十五个配方从没量到过的判据(它的入口是
content.find("xpm")—— 文件里第一次出现这三个字母,而注释里就有)Windows 形态、以及一个「每条规则都为本宿主编译过」的夹具 —— 它第一批运行就抓到三处
mcpp:plugins0.2.5沙箱验证(已跑,对着已发布物)
xlings subos use ... --sandbox,CN 镜像,空$HOME、新/tmp、PATH 上没有 mcpp:本次发布后会用新的 tarball 再跑一次。