Skip to content

Commit 6cced75

Browse files
authored
图形离屏示例,以及两处只在 Linux 之外成立的缺陷 (#586)
一句话:**引擎与规则层早就与平台无关,而生态只在 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)—— 已修 Provisioning [xlings.workspace] entries declared by dependencies (xim:shaderc@>=2026.3) The filename, directory name, or volume label syntax is incorrect. `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 的 macOS libc++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 的构造 —— 它们不该被 「补到三平台」。「尚未实现」补两条按平台分的缺口,都写明缺的具体是什么。 ## 生态侧(已合入) * xim-pkgindex #778:`shaderc` 补 macOS arm64 与 Windows x86_64 * xim-pkgindex #779:CUDA 四个组件 + `dpcpp` 补 windows 段;共享 install/config 形状里 三处按宿主分的支;一条量了很久却对十五个配方从没量到过的判据(它的入口是 `content.find("xpm")` —— 文件里第一次出现这三个字母,而注释里就有) * mcpp-plugins #9 → 0.2.5:着色器阶段冲突的拒绝、三平台各自的编译器、CUDA/SYCL 的 Windows 形态、以及一个「每条规则都为本宿主编译过」的夹具 —— 它第一批运行就抓到三处 * mcpp-index #365:`mcpp:plugins` 0.2.5 ## 沙箱验证(已跑,对着已发布物) `xlings subos use ... --sandbox`,CN 镜像,空 `$HOME`、新 `/tmp`、PATH 上没有 mcpp: ok: Provisioning [xlings.workspace] entries declared by dependencies (xim:glslang@>=15.1.0) ok: both stages: 354 + 124 words ok: glslang: 15.1.0 (恰好一个版本) 本次发布后会用新的 tarball 再跑一次。
1 parent 8135804 commit 6cced75

27 files changed

Lines changed: 1890 additions & 24 deletions

‎.agents/docs/2026-09-07-heterogeneous-cross-platform-ecosystem.md‎

Lines changed: 383 additions & 0 deletions
Large diffs are not rendered by default.

‎.github/tools/build_examples.sh‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ SKIP=(
5151
"examples/09-heterogeneous/hip/app|same, for the HIP payloads"
5252
"examples/09-heterogeneous/sycl/app|needs the dpcpp payload (over a gigabyte) and a device its runtime accepts"
5353
"examples/09-heterogeneous/vulkan/app|built AND RUN by the next step of this job, on the lavapipe payload, which needs no GPU"
54+
"examples/10-graphics/offscreen|built AND RUN by its own step of this job, on the same lavapipe payload. It renders a real graphics pipeline offscreen and asserts the pixels, which is why it runs there rather than here"
5455
"examples/09-heterogeneous/cann/app|its device leg needs the Ascend DRIVER, which a runner does not have: the kernel compiles and the object links, and then `libascend_hal.so` is missing -- and only that one, since 2026.9.6.6 models DT_RPATH inheritance. Correct on a machine with no NPU. Its CPU leg does build, and is not built here only because it would make this job resolve a fifth rule package for one example. Covered by the measurements in its README"
5556
)
5657

‎.github/workflows/ci-linux.yml‎

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,65 @@ jobs:
285285
export MCPP_VENDORED_XLINGS="$XLINGS_BIN"
286286
bash .github/tools/build_examples.sh
287287
288+
- name: "Graphics example: render offscreen on lavapipe and assert the pixels"
289+
run: |
290+
export MCPP_VENDORED_XLINGS="$XLINGS_BIN"
291+
cd examples/10-graphics/offscreen
292+
"$MCPP" build
293+
# THE SHADERS ARE THE FIRST ASSERTION, and both of them: a rule that
294+
# compiled only the first source would leave the second header absent
295+
# and everything after this would still pass.
296+
for f in triangle_vert triangle_frag; do
297+
d="target/.build-mcpp/out/spirv"
298+
test -f "$d/$f.h" || { echo "missing $d/$f.h"; exit 1; }
299+
# Either file: which of the two carries the words is a property of
300+
# the shader compiler the rule chose, not of the shader. See the
301+
# cross-platform jobs, where that choice differs.
302+
# ONE FILE AT A TIME, because `grep -qs a b` exits 2 when `b`
303+
# does not exist -- even on a match in `a`, and even with `-s`,
304+
# which suppresses the message and not the status. Written as one
305+
# grep over both names, this criterion fails whenever the route
306+
# that produces only a header is taken, which is a failure about
307+
# the criterion and not about the shader.
308+
found=""
309+
for g in "$d/$f.h" "$d/$f.inc"; do
310+
[ -f "$g" ] && grep -q '0x07230203' "$g" && found=1
311+
done
312+
[ -n "$found" ] \
313+
|| { echo "$f carries no SPIR-V magic in either $f.h or $f.inc"; exit 1; }
314+
done
315+
icd=$(find "${MCPP_HOME:-$HOME/.mcpp}/registry/data/xpkgs/xim-x-mesa-lavapipe" \
316+
"$HOME/.xlings/data/xpkgs/xim-x-mesa-lavapipe" \
317+
-name 'lvp_icd.x86_64.json' -print -quit 2>/dev/null || true)
318+
[ -n "$icd" ] || { echo "no lavapipe ICD in either store"; exit 1; }
319+
out=$(VK_DRIVER_FILES="$icd" "$MCPP" run 2>&1) || { echo "$out"; exit 1; }
320+
echo "$out"
321+
# The program asserts the corners and the centre itself and exits
322+
# non-zero on either. What CI adds is that the run reached the DEVICE:
323+
# the software rasteriser produces the same pixels by construction, so
324+
# the image cannot distinguish them and the device name is what does.
325+
echo "$out" | grep -q 'llvmpipe' \
326+
|| { echo "the run did not reach the lavapipe device"; exit 1; }
327+
echo "$out" | grep -qE 'centre pixel: \([0-9]+, [0-9]+, [0-9]+, 255\)' \
328+
|| { echo "no centre pixel was reported"; exit 1; }
329+
# THE REVERSE LEG, AND IT COMPARES THE PIXELS RATHER THAN JUST
330+
# RUNNING. The claim this example makes is that the image is a
331+
# contract two independent rasterisers satisfy, not a property of one
332+
# device -- so the criterion is that the two legs report the SAME
333+
# centre pixel while reporting different devices. Measured: both give
334+
# (124, 70, 62, 255), byte for byte.
335+
gpu_px=$(echo "$out" | grep -m1 '^centre pixel:')
336+
"$MCPP" build --no-accel
337+
"$MCPP" run --no-accel | tee cpu.log
338+
grep -q 'cpu rasteriser' cpu.log || { echo "the CPU leg did not run"; exit 1; }
339+
cpu_px=$(grep -m1 '^centre pixel:' cpu.log)
340+
[ -n "$gpu_px" ] && [ "$gpu_px" = "$cpu_px" ] || {
341+
echo "the two legs disagree about the image:"
342+
echo " device: $gpu_px"
343+
echo " cpu: $cpu_px"
344+
exit 1; }
345+
echo "ok: both legs agree on $cpu_px"
346+
288347
- name: "Vulkan example: build the device half and run it on lavapipe"
289348
run: |
290349
export MCPP_VENDORED_XLINGS="$XLINGS_BIN"

‎.github/workflows/ci-macos.yml‎

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -310,6 +310,57 @@ jobs:
310310
"$MCPP" build
311311
"$MCPP" --version
312312
313+
314+
# GRAPHICS ON THIS HOST, BUILD ONLY, AND THAT IS THE WHOLE CLAIM.
315+
#
316+
# This runner has no Vulkan device, so what is asserted is what this
317+
# platform decides: that the shader compiler THIS platform uses -- the
318+
# rule declares `xim:shaderc` here and `xim:glslang` on Linux -- produces
319+
# both SPIR-V headers, and that the Vulkan half compiles and links
320+
# against the loader package. Running it is the Linux job's criterion,
321+
# where a software device (`xim:mesa-lavapipe`) is published and the two
322+
# legs' pixels are compared.
323+
#
324+
# The example is otherwise built only on Linux (`build_examples.sh` runs
325+
# there), which is exactly the shape this change exists to remove: the
326+
# half of a lane written for a host is the half that host never
327+
# exercises.
328+
- name: "Graphics: the offscreen example builds on this host"
329+
shell: bash
330+
run: |
331+
set -e
332+
export MCPP_VENDORED_XLINGS="$XLINGS_BIN"
333+
cd "$GITHUB_WORKSPACE/examples/10-graphics/offscreen"
334+
# The toolchain is NAMED rather than inherited: leaving it to whatever
335+
# a neighbouring step happened to select makes this step's subject
336+
# depend on step order, which is not a property anybody reads.
337+
"/tmp/mcpp-fresh" build --toolchain "llvm@${MCPP_LLVM_VER}"
338+
for f in triangle_vert triangle_frag; do
339+
d="target/.build-mcpp/out/spirv"
340+
test -f "$d/$f.h" || { echo "missing $d/$f.h"; exit 1; }
341+
# THE MAGIC IS NOT ALWAYS IN THE HEADER, AND THAT IS THE POINT OF
342+
# THIS JOB. The rule chooses the shader compiler this platform
343+
# publishes -- glslang on Linux, glslc here -- and the two split
344+
# the declaration differently: glslang writes a complete `const
345+
# uint32_t ...[] = {...}`, glslc an initialiser list the rule
346+
# declares around, so the words land in `<stem>.inc`. An assertion
347+
# naming only the header is an assertion about ONE compiler, which
348+
# is exactly the shape this step exists to catch.
349+
# ONE FILE AT A TIME, because `grep -qs a b` exits 2 when `b`
350+
# does not exist -- even on a match in `a`, and even with `-s`,
351+
# which suppresses the message and not the status. Written as one
352+
# grep over both names, this criterion fails whenever the route
353+
# that produces only a header is taken, which is a failure about
354+
# the criterion and not about the shader.
355+
found=""
356+
for g in "$d/$f.h" "$d/$f.inc"; do
357+
[ -f "$g" ] && grep -q '0x07230203' "$g" && found=1
358+
done
359+
[ -n "$found" ] \
360+
|| { echo "$f carries no SPIR-V magic in either $f.h or $f.inc"; exit 1; }
361+
done
362+
echo "ok: both shader stages compiled and the Vulkan half linked"
363+
313364
# Integration: the mcpp built from THIS PR's source (the self-host binary,
314365
# $MCPP = /tmp/mcpp-fresh) builds & runs a real external C++ project —
315366
# xlings (openxlings/xlings ships its own mcpp.toml).

‎.github/workflows/ci-windows.yml‎

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -405,6 +405,57 @@ jobs:
405405
# restore the LLVM default for the remaining steps
406406
"$MCPP_SELF" toolchain default llvm@20.1.7
407407
408+
409+
# GRAPHICS ON THIS HOST, BUILD ONLY, AND THAT IS THE WHOLE CLAIM.
410+
#
411+
# This runner has no Vulkan device, so what is asserted is what this
412+
# platform decides: that the shader compiler THIS platform uses -- the
413+
# rule declares `xim:shaderc` here and `xim:glslang` on Linux -- produces
414+
# both SPIR-V headers, and that the Vulkan half compiles and links
415+
# against the loader package. Running it is the Linux job's criterion,
416+
# where a software device (`xim:mesa-lavapipe`) is published and the two
417+
# legs' pixels are compared.
418+
#
419+
# The example is otherwise built only on Linux (`build_examples.sh` runs
420+
# there), which is exactly the shape this change exists to remove: the
421+
# half of a lane written for a host is the half that host never
422+
# exercises.
423+
- name: "Graphics: the offscreen example builds on this host"
424+
shell: bash
425+
run: |
426+
set -e
427+
export MCPP_VENDORED_XLINGS="$XLINGS_BIN"
428+
cd "$GITHUB_WORKSPACE/examples/10-graphics/offscreen"
429+
# The toolchain is NAMED rather than inherited: leaving it to whatever
430+
# a neighbouring step happened to select makes this step's subject
431+
# depend on step order, which is not a property anybody reads.
432+
"$MCPP_SELF" build --toolchain "llvm@20.1.7"
433+
for f in triangle_vert triangle_frag; do
434+
d="target/.build-mcpp/out/spirv"
435+
test -f "$d/$f.h" || { echo "missing $d/$f.h"; exit 1; }
436+
# THE MAGIC IS NOT ALWAYS IN THE HEADER, AND THAT IS THE POINT OF
437+
# THIS JOB. The rule chooses the shader compiler this platform
438+
# publishes -- glslang on Linux, glslc here -- and the two split
439+
# the declaration differently: glslang writes a complete `const
440+
# uint32_t ...[] = {...}`, glslc an initialiser list the rule
441+
# declares around, so the words land in `<stem>.inc`. An assertion
442+
# naming only the header is an assertion about ONE compiler, which
443+
# is exactly the shape this step exists to catch.
444+
# ONE FILE AT A TIME, because `grep -qs a b` exits 2 when `b`
445+
# does not exist -- even on a match in `a`, and even with `-s`,
446+
# which suppresses the message and not the status. Written as one
447+
# grep over both names, this criterion fails whenever the route
448+
# that produces only a header is taken, which is a failure about
449+
# the criterion and not about the shader.
450+
found=""
451+
for g in "$d/$f.h" "$d/$f.inc"; do
452+
[ -f "$g" ] && grep -q '0x07230203' "$g" && found=1
453+
done
454+
[ -n "$found" ] \
455+
|| { echo "$f carries no SPIR-V magic in either $f.h or $f.inc"; exit 1; }
456+
done
457+
echo "ok: both shader stages compiled and the Vulkan half linked"
458+
408459
- name: "Toolchain: LLVM — build mcpp (self-host)"
409460
shell: bash
410461
run: |

‎CHANGELOG.md‎

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,65 @@
55
66
## [Unreleased]
77

8+
## [2026.9.7.1] - 2026-09-07
9+
10+
### 两处只在 Linux 之外成立的缺陷,以及第一条图形管线
11+
12+
**引擎与规则层早就与平台无关,而生态只在 Linux 上完整。** 把规则包在另外两个平台上
13+
真跑一遍,暴露了两处引擎缺陷 —— 两处的共同形状是:一段代码的正确性依赖于宿主,而 CI
14+
只在其中一个宿主上执行它。
15+
16+
**macOS 14 上构建程序用不了 `std::println`,而显而易见的修法更糟(已记录,未修)。**
17+
`host_link_tokens` 的「信任 cfg」出口不给 `Toolchain::linkRuntimeDirs` 发 `-L`,于是
18+
macOS 上 `-lc++` 经 SDK 解析到**系统**那份,而头文件来自载荷。macos-14 上编译一个只有
19+
`import std` 的构建程序:
20+
21+
ld64.lld: error: undefined symbol: std::__1::__is_posix_terminal(__sFILE*)
22+
23+
`std::print` 不是 header-only 的,它的两个重载都要到 libc++ **dylib** 里取支持符号,
24+
而那两个符号是在 macOS 14 不带的那一版里加进去的。macOS 15 有,所以这个项目用到的每
25+
一台 macOS runner 都是绿的。
26+
27+
在这一条上加 `-L<载荷>/lib` 试过了,**它买来一个更糟的问题**:那会让 `-lc++` 解析到
28+
工具链自己的 dylib,也就是 `dist::mechanism_for` 在 Mach-O 上明确拒绝的
29+
ToolchainCoupled —— LLVM 的 macOS libc++abi 与 libunwind dylib **向上链接**
30+
`/usr/lib/libc++`,于是系统 libc++ 与工具链的那份同时载入,跨两份释放的对象在
31+
libmalloc 里 abort(#202)。CI 报的正是这条路的第一步:链接停在 `__cxa_end_catch`
32+
与其余那些系统 libc++ 会再导出、而载荷那份不会的 ABI 符号上。
33+
34+
所以这个宿主上的 C++ 运行时就是系统那一份,而下限由分发契约选的**静态 libc++** 变成
35+
真的 —— 那是产物的机制,不属于一个 mcpp 编译、就地跑一次、然后丢掉的辅助程序。
36+
**限制照实写下来:macOS 14 上构建程序不能用 `std::print` / `std::println`。**
37+
`std::format` 是 header-only 的,没有这个问题;本轮 mcpp:plugins 的六个模块因此全部
38+
改用它。判据 `HostFlags.OnlyTheSpelledOutExitNamesTheToolchainRuntimeDirs` 把这条
39+
不对称写成了一条会跑的断言。
40+
41+
**版本约束里的 `>` 被 cmd.exe 读成重定向(Windows)。** mcpp 把供给请求作为 JSON 参数
42+
放在 shell 命令行上;`shell::quote` 回答的是子进程的 argv 解析(`\"`),而 cmd 不认这
43+
个转义,于是走到 `>` 时引号数是偶数,`>` 成了重定向:
44+
45+
Provisioning [xlings.workspace] entries declared by dependencies (xim:shaderc@>=2026.3)
46+
The filename, directory name, or volume label syntax is incorrect.
47+
48+
`>=` 正是每个规则包声明下界用的形态,而在此之前没有任何一条能在 Windows 上生效的声明
49+
带过 `>`。修法是标准的双重转义,判据是两个解析器的模拟器加一条反向腿。
50+
51+
### `examples/10-graphics/offscreen`:第一条图形管线,判据是像素
52+
53+
此前所有异构示例都是计算。这一个是图形:顶点与片段两个着色器阶段、一条 render pass、
54+
`vkCmdDraw` 一个三角形、`vkCmdCopyImageToBuffer` 取回像素。**离屏而不是开窗**,因为那
55+
是可断言的形态。同一道接缝背后是一个自己写的软件光栅器,而两条腿的中心像素**逐字节
56+
相同** —— 图像是契约,设备名是唯一区分它们的东西。
57+
58+
这个示例挖出一条使用者会撞上的规则:**依赖不能被 layer 条件化**。
59+
`cfg(accelerator = ...)` 下的 `[build]` 源生效而依赖被忽略,于是包被丢掉、包含它的源
60+
被留下。
61+
62+
### 文档
63+
64+
`docs/20` 新增「每条 lane 到得了哪些平台」:三件事同时为真才叫一条 lane 在某个平台上
65+
成立,而第三件(规则自己那段按宿主分岔的代码编译得过)是最容易被默认成立的那一件。
66+
867
## [2026.9.6.6] - 2026-09-07
968

1069
### 一个包一个版本:规则自带环境,工程只写例外

‎docs/01-examples.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ examples.
3838
| 09d | [`…/hip`](../examples/09-heterogeneous/hip/) | The same computation in HIP, reaching an NVIDIA device | `mcpp.rules.hip`, HIP as a header layer over the CUDA runtime, a two-chunk `accel` |
3939
| 09e | [`…/multi-backend`](../examples/09-heterogeneous/multi-backend/) | Several backends in ONE artifact, chosen at run time — the library shape, not the program shape | `accel` as a set, `cfg(accelerator = "none")` and its negation, a dispatch chain, a module seam over a C island boundary |
4040
| 09f | [`…/cann`](../examples/09-heterogeneous/cann/) | An Ascend C kernel behind the same seam. **Does not build yet** — its README names the two missing pieces | the `.asc` device extension, `op_kernel`/`op_host` as an island CANN already has, `accelerator = "none"` for the fallback |
41+
| 10 | [`examples/10-graphics`](../examples/10-graphics/) | Graphics rather than compute: a rendering pipeline whose result is pixels | `mcpp.rules.spirv` for the vertex and fragment stages, offscreen rendering as the assertable form |
42+
| 10a | [`…/offscreen`](../examples/10-graphics/offscreen/) | A triangle rasterised by Vulkan into a buffer, and the same triangle by a software rasteriser behind the same seam | two shader stages from one glob, a render pass with no window or swapchain, a pixel as the criterion |
4143

4244
## Suggested Reading Order
4345

0 commit comments

Comments
 (0)