Skip to content

Commit 30b5b6d

Browse files
committed
docs: 实现记录与对设计的四处更正,沙箱验证扩到渲染链
设计写完就动手了,§10 记录做出来的东西和**被实现推翻的部分** —— 后者更重要,每一条都是 被实测否掉的: G3「新增仓 0」错了一半。wayland-protocols 是另一个上游、另一个版本(1.49),而 mcpplibs/wayland 的 upstream/ 是 wayland 1.26.0 且 CI 逐字节 diff。一个仓一个上游一个 版本,所以必须独立 fork。新增仓 = 1。 G3「一个包装 65 个协议」也错了,而且原因不是体积是链接:staging 与 unstable 携带同一 协议的不同成熟度,scanner 生成同名符号。stable∩unstable=13,其余两两为 0,tier 内为 0 —— 所以是三个包,边界是上游自己的目录结构。由此还带出「三个已被取代的 unstable 协议 不能发」和「staging/unstable 依赖 stable,于是消费者不能同时写两个」。体积也量了,与 担心相反:65 个协议的 .c 只有 5910 行编出 270KB,3.5MB 几乎全是头文件。 G2 的形态被推翻两次。接上既有共享目录机制之后 GPU 到位了,但 NVIDIA 枚举两次 —— sentinel 补上 libXext 之后宿主那份 ICD 也能加载了。VK_DRIVER_FILES 能消掉重复(实测 恰好 1 个设备,而且它接受目录),但现在不能做:xim:mesa 只带一个 radeon ICD,声明它 等于拿「NVIDIA 重复」换「Intel 上完全没有 Vulkan」。 G4 pixman 的分类对了理由错了。文件确实自带 ifdef 门控,但编译标志是逐文件的,包级 cflags 表达不了。真正的答案是 [build] flags 带 glob。由此把 build.mcpp 的边界划细了 一层:按目标变的是「编哪些文件」用 build.mcpp,变的是「给哪个文件什么标志」用 glob。 沙箱脚本加了 GLES2 渲染并重跑:整条链从打开 DRM 节点走到读回自己画的像素,两个节点 都 64 128 191 255 分毫不差,而宿主的图形库在场且可达却一条都没赢。这是本轮之前做不到 的那一步 —— §19.4 那次止于 eglInitialize,能初始化不等于能画。
1 parent ac93354 commit 30b5b6d

2 files changed

Lines changed: 216 additions & 4 deletions

File tree

‎.agents/docs/2026-08-30-graphics-stack-coverage-design.md‎

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -390,3 +390,172 @@ G6 libudev 路线决策 → libseat ← 最后,单独评估
390390
> **G1a 三个成员是分水岭 —— 做完它,mcpp 从「EGL 能初始化」变成「能画」;
391391
> 其余都是补齐,唯一的架构性问题是 Vulkan 的 `XDG_DATA_DIRS` 目录合并,
392392
> 而那一行改在 xim-pkgindex,不在这里。**
393+
394+
---
395+
396+
## 10. 实现结果与对本设计的更正(2026-08-30 执行记录)
397+
398+
设计写完就动手了。这一节记录**做出来的东西**,以及**设计里被实现推翻的部分** ——
399+
后者更重要,因为每一条都是被实测否掉的。
400+
401+
### 10.1 交付
402+
403+
| # | 交付 | 状态 |
404+
|---|---|---|
405+
| G1a | mcpplibs/libglvnd 加 `mcpp/{glesv2,glesv1,opengl}` 三个成员 | `v1.7.0`,CI 全绿 |
406+
| G1b | `khronos.{glesv2,glesv1,opengl}` 模块层 | 同上,与 C 库一起做完 |
407+
| G2 | xim-pkgindex `nvidia-gl-host-link` 0.1.3 声明 Vulkan ICD | PR #731,已实测 |
408+
| G3 | **mcpplibs/wayland-protocols**(新建)三个 tier | `1.49`,CI 待绿 |
409+
| G4a | `compat.pixman` 0.46.4 | 已实测 |
410+
| 索引 | `freedesktop.{glesv2,glesv1,opengl}`、三个 `wayland-protocols-*`、`compat.pixman` | PR #294 |
411+
| 镜像 | libglvnd / wayland-protocols / pixman 三份 CN,均已核对 sha256 | 完成 |
412+
413+
### 10.2 被推翻的:G3 的「新增仓 0」
414+
415+
设计 §6 写着「新增仓数量 = 0 —— G1 复用 libglvnd,G3 做 wayland fork 的第五个成员」。
416+
**G3 那半是错的。**
417+
418+
`mcpplibs/wayland` 的 `upstream/` 是 wayland **1.26.0** 且 CI 逐字节 diff;
419+
wayland-protocols 是**另一个上游项目、另一个版本(1.49)**。一个仓一个上游一个版本,
420+
所以它必须是独立 fork。**新增仓数量 = 1。**
421+
422+
### 10.3 被推翻的:G3 的「一个包装全部 65 个协议」
423+
424+
设计说「全量预生成」,只提了体积。**体积不是问题,链接才是。**
425+
426+
```
427+
multiple definition of `zwp_linux_dmabuf_v1_interface'
428+
```
429+
430+
staging/ 与 unstable/ 携带**同一个协议的不同成熟度**,scanner 为两者生成**同名符号**。
431+
按导出的 `wl_interface` 数清点:
432+
433+
| | |
434+
|---|---|
435+
| stable ∩ staging | **0** |
436+
| stable ∩ unstable | **13** |
437+
| staging ∩ unstable | **0** |
438+
| 任一 tier 内部 | **0** |
439+
440+
所以是**三个包,按 tier 分** —— 而这个边界是上游自己的目录结构,不是发明出来的。
441+
442+
由此还带出两条只有动手才会发现的事:
443+
444+
1. **三个 unstable 协议不能发**:`xdg-shell-unstable-v5`、`linux-dmabuf-unstable-v1`、
445+
`tablet-unstable-v2` —— 它们**就是**那 13 个重叠符号,而且各自都已被同名 stable 协议
446+
取代。上游留着只为兼容,一个包不可能两个都发。
447+
2. **staging/unstable 依赖 stable**,实测:两者都引用 `xdg_toplevel_interface`,staging
448+
还引用 `zwp_tablet_tool_v2_interface`。fork 内用 path 依赖,于是**消费者不能同时写
449+
staging 和 stable** —— mcpp 报「既是 version dep 又是 path dep」。规则:要 staging
450+
就只写 staging。
451+
452+
体积那一问也量了,而且答案与担心相反:65 个协议的 `.c` 一共 **5910 行,编出 270KB**;
453+
3.5MB 几乎全是**头文件**,不 include 就不花钱。
454+
455+
### 10.4 被推翻两次的:G2 的形态
456+
457+
设计 §3 已经把 `VK_DRIVER_FILES` 标成「作废,应当接既有的共享目录机制」。**接完发现两者
458+
是互补的,不是二选一** —— 但第二个仍然**现在不能做**。
459+
460+
实测三段:
461+
462+
```
463+
A 改动前 devices = 1 → llvmpipe (LLVM 20.1.2)
464+
B sentinel 声明 Vulkan ICD devices = 3 → NVIDIA RTX 4080 ×2 + llvmpipe
465+
C 再加 VK_DRIVER_FILES devices = 1 → NVIDIA RTX 4080
466+
```
467+
468+
B 是本轮做的:GPU 到位了。但 NVIDIA **出现两次** —— sentinel 补上 `libXext` 之后,
469+
宿主 `/usr/share` 里那份裸 soname 的 ICD 也能加载了。C 能消掉重复(而且
470+
`VK_DRIVER_FILES` **接受目录**,一行 DISCOVERY 就够)。
471+
472+
**C 现在不能做**:`xim:mesa` 的 payload 只带 `radeon_icd.x86_64.json` 一个 ICD。声明
473+
`VK_DRIVER_FILES` 会让生态集合成为唯一权威,而在 Intel 机器上那个集合是空的 —— 等于拿
474+
「NVIDIA 重复枚举」换「Intel 上完全没有 Vulkan」。前置条件是 mesa 把它构建的 ICD 都发
475+
出来(anv / lvp / nouveau),那是 payload 的问题。
476+
477+
### 10.5 被修正的:G4 pixman「SIMD 自带门控」
478+
479+
设计说 pixman 的 SIMD「自带 `#ifdef` 门控,不需要 build.mcpp」。**分类(A 类内联)对了,
480+
理由错了。**
481+
482+
文件确实自带门控,但**编译标志是逐文件的**:上游每个指令集建一个静态库,各自带
483+
`-msse2` / `-mssse3`。包级 cflags 表达不了 —— `-mssse3` 加到所有文件上,编译器就可能在
484+
`pixman-x86.c` 的 CPUID 检查**之前**发出 SSSE3 指令。
485+
486+
真正的答案是 **`[build] flags` 带 `glob`**,而且不是新机制:`compat.sdl2` 早就用它把
487+
`-msse3` 限定到单个文件。实测标志没有外溢:
488+
489+
```
490+
pixman-ssse3.o SSSE3 指令 2 ← 应当有
491+
pixman-sse2.o SSSE3 指令 0
492+
pixman.o SSSE3 指令 0
493+
pixman-fast-path.o SSSE3 指令 0
494+
pixman-x86.o SSSE3 指令 0
495+
```
496+
497+
**由此得到一条判据补充**,值得记住,因为它把 build.mcpp 的边界又划细了一层:
498+
499+
> 按目标变的是**编哪些文件** → `build.mcpp`(GLdispatch 的 entry stub)。
500+
> 按目标变的是**给哪个文件什么标志** → `[build] flags` 的 `glob`(pixman 的 SIMD)。
501+
502+
### 10.6 G1 的三个坑(都已写进代码注释)
503+
504+
1. **`genglmod.sh` 必须 `LC_ALL=C`。** CI 上重新生成得到**一样的符号数**(358/145/653)
505+
却有 48 行 glesv2 和 88 行 opengl 不同 —— `sort` 按 locale 排序混合大小写标识符,
506+
开发机 en_US.UTF-8 与 runner 的 C 顺序不同。同样的输入,不同的文件。
507+
2. **包内测试无法证明 `.so` 身份。** 它链接的是本包的**对象**,所以 test 二进制自己定义
508+
`glClear`,dladdr 报的是可执行文件 —— 一条「不是 payload 的副本」的断言会在从未看过
509+
任何库的情况下通过。`.so` 级身份检查属于**消费者**,在 mcpp-index 的成员里。
510+
3. **三个 GL flavour 符号面重叠**(都导出 `glClear`)。同一进程链两个,名字绑到先加载的
511+
那个 —— 实测:一个想用 GLESv2 的程序里 `glClear` 落到了 `libGLESv1_CM.so.1`,毫无
512+
提示。所以索引测试成员只依赖一个 flavour,并断言 `glClear` 确实解析在它里面。
513+
514+
模块层那个「GL 入口是否外部链接」的风险**解除了**:`KHRONOS_APICALL` 在 Linux 分支展开
515+
为空,所以 `GL_APICALL void GL_APIENTRY glClear(...)` 就是普通外部链接声明,和 EGL 一样
516+
不需要 forwarder。
517+
518+
### 10.7 pixman 的一个静默坑
519+
520+
`PIXMAN_API` 定义在 **`pixman-version.h.in`** 里,不在编译器头里。生成 version.h 时漏掉
521+
它,`pixman.h` 里每个 `PIXMAN_API void pixman_fill(...)` 都解析成未知标识符、声明整个
522+
丢失 —— 而报出来的错误是某个 SIMD 文件里的
523+
`implicit declaration of function 'pixman_fill'`,与真正的原因隔了十万八千里。
524+
525+
### 10.8 沙箱验证已扩到渲染链,并重跑
526+
527+
签进仓的 `tests/verify_graphics_closed_loop_sandbox.sh` 加了 GLES2 渲染,在合成 home、
528+
清空 store、从零构建的沙箱里对分支重跑(`--sandbox --gpu`):
529+
530+
```
531+
===== 6. did anything come from the host? =====
532+
PASS: the host.s copies were present and reachable, and none of them won
533+
534+
===== 7. run it =====
535+
/dev/dri/renderD128 drm driver nvidia-drm
536+
eglInitialize EGL 1.5, vendor Mesa Project
537+
GL_VERSION OpenGL ES 3.2 Mesa 25.0.7
538+
glReadPixels 64 128 191 255 (wanted 64 128 191 255)
539+
/dev/dri/card0 drm driver simpledrm
540+
gbm_bo_create 256x256 stride=1024
541+
GL_VERSION OpenGL ES 3.2 Mesa 25.0.7
542+
glReadPixels 64 128 191 255 (wanted 64 128 191 255)
543+
544+
reached EGL on a real device: yes
545+
drew and read the pixel back: yes
546+
RESULT: PASS
547+
```
548+
549+
**这是本轮之前做不到的那一步。** §19.4 那次止于 `eglInitialize` —— 能初始化不等于能画。
550+
现在闭环从「打开 DRM 节点」一路走到「读回自己画的像素」,而且是在宿主图形库在场且可达
551+
的沙箱里。
552+
553+
### 10.9 仍未做
554+
555+
- **G4b `libxkbcommon`**:独立项目,内联描述符可行,但运行期还需要 `xkeyboard-config`
556+
的**数据文件**,那是第二个包(纯数据)。
557+
- **G5 `libinput`**:拖 `libevdev` / `mtdev` / `libudev`。
558+
- **G6 `libudev` / `libseat`**:systemd 邻域,设计里就写明「先不做,单独评估」。
559+
560+
前两条是普通工作量;G6 是需要决策的。合成器可以在「已有 DRM master」的前提下开发
561+
(从 TTY 直接启动、或 `SEATD_SOCK`),把 session 管理留到最后。

‎tests/verify_graphics_closed_loop_sandbox.sh‎

Lines changed: 47 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ libgbm = "25.0.7"
9595
9696
[target.'cfg(linux)'.dependencies.freedesktop]
9797
egl = "1.7.0"
98+
glesv2 = "1.7.0"
9899
wayland = "1.26.0"
99100
wayland-server = "1.26.0"
100101
TOML
@@ -113,7 +114,10 @@ cat > "$W/src/main.cpp" <<'CPP'
113114
#include <cstring>
114115
#include <initializer_list>
115116
117+
#include <GLES3/gl32.h>
118+
116119
import khronos.egl;
120+
import khronos.glesv2;
117121
import freedesktop.wayland.client;
118122
import freedesktop.wayland.server;
119123
@@ -129,7 +133,7 @@ int main()
129133
} else { std::puts(" wl_display_create FAILED"); return 1; }
130134
131135
std::puts(" -- DRM node -> GBM device -> EGL display --");
132-
int reached = 0;
136+
int reached = 0, drew = 0;
133137
for (const char *node : {"/dev/dri/renderD128", "/dev/dri/card0"}) {
134138
int fd = ::open(node, O_RDWR);
135139
if (fd < 0) { std::printf(" %-22s (no access)\n", node); continue; }
@@ -155,6 +159,44 @@ int main()
155159
std::printf(" eglInitialize EGL %d.%d, vendor %s\n",
156160
ma, mi, eglQueryString(d, EGL_VENDOR));
157161
reached = 1;
162+
163+
// AND THEN DRAW. Reaching eglInitialize only proves the
164+
// dispatch is live; a stack that cannot render gets this
165+
// far too. A surfaceless context (GBM has no pbuffer
166+
// configs) into an FBO, clear it, read the pixel back.
167+
eglBindAPI(EGL_OPENGL_ES_API);
168+
EGLint ca[] = { EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT,
169+
EGL_RED_SIZE, 8, EGL_GREEN_SIZE, 8,
170+
EGL_BLUE_SIZE, 8, EGL_ALPHA_SIZE, 8, EGL_NONE };
171+
EGLConfig cfg{}; EGLint n = 0;
172+
EGLint xa[] = { EGL_CONTEXT_CLIENT_VERSION, 2, EGL_NONE };
173+
if (eglChooseConfig(d, ca, &cfg, 1, &n) && n > 0) {
174+
EGLContext ctx = eglCreateContext(d, cfg, EGL_NO_CONTEXT, xa);
175+
if (ctx != EGL_NO_CONTEXT &&
176+
eglMakeCurrent(d, EGL_NO_SURFACE, EGL_NO_SURFACE, ctx)) {
177+
GLuint rb = 0, fbo = 0;
178+
glGenRenderbuffers(1, &rb);
179+
glBindRenderbuffer(GL_RENDERBUFFER, rb);
180+
glRenderbufferStorage(GL_RENDERBUFFER, GL_RGBA8, 64, 64);
181+
glGenFramebuffers(1, &fbo);
182+
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
183+
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
184+
GL_RENDERBUFFER, rb);
185+
std::printf(" GL_VERSION %s\n", glGetString(GL_VERSION));
186+
glClearColor(0.25f, 0.5f, 0.75f, 1.0f);
187+
glClear(GL_COLOR_BUFFER_BIT);
188+
glFinish();
189+
unsigned char px[4] = {0,0,0,0};
190+
glReadPixels(32, 32, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, px);
191+
std::printf(" glReadPixels %u %u %u %u (wanted 64 128 191 255)\n",
192+
px[0], px[1], px[2], px[3]);
193+
drew = (px[0] == 64 && px[1] == 128 && px[2] == 191 && px[3] == 255);
194+
glDeleteFramebuffers(1, &fbo);
195+
glDeleteRenderbuffers(1, &rb);
196+
eglMakeCurrent(d, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
197+
}
198+
if (ctx != EGL_NO_CONTEXT) eglDestroyContext(d, ctx);
199+
}
158200
eglTerminate(d);
159201
}
160202
}
@@ -163,6 +205,7 @@ int main()
163205
::close(fd);
164206
}
165207
std::printf("\n reached EGL on a real device: %s\n", reached ? "yes" : "no");
208+
std::printf(" drew and read the pixel back: %s\n", drew ? "yes" : "no");
166209
return 0;
167210
}
168211
CPP
@@ -175,14 +218,14 @@ say "5. what the loader actually resolved"
175218
BIN=$(find "$W/target" -name closed-loop -type f -perm -u+x | head -1)
176219
[ -n "$BIN" ] || { echo "FAIL: no binary"; exit 1; }
177220
I=$(readelf -p .interp "$BIN" | grep -oE '/[^ ]*ld-linux[^ ]*')
178-
"$I" --list "$BIN" | grep -E 'libEGL|libGLdispatch|libgbm|libdrm|libwayland|libffi|libexpat' \
221+
"$I" --list "$BIN" | grep -E 'libEGL|libGLESv2|libGLdispatch|libgbm|libdrm|libwayland|libffi|libexpat' \
179222
| sed "s|$SUBOS|<subos>|g; s|$W|<project>|g"
180223

181224
say "6. did anything come from the host?"
182-
if "$I" --list "$BIN" | grep -E 'libEGL|libGLdispatch|libgbm|libdrm|libwayland|libffi|libexpat' \
225+
if "$I" --list "$BIN" | grep -E 'libEGL|libGLESv2|libGLdispatch|libgbm|libdrm|libwayland|libffi|libexpat' \
183226
| grep -qE '=> /(usr/)?lib/'; then
184227
echo " FAIL: a graphics library resolved to the host"
185-
"$I" --list "$BIN" | grep -E 'libEGL|libGLdispatch|libgbm|libdrm|libwayland' | grep -E '=> /(usr/)?lib/'
228+
"$I" --list "$BIN" | grep -E 'libEGL|libGLESv2|libGLdispatch|libgbm|libdrm|libwayland' | grep -E '=> /(usr/)?lib/'
186229
exit 1
187230
fi
188231
echo " PASS: the host's copies were present and reachable, and none of them won"

0 commit comments

Comments
 (0)