Skip to content

Commit 2f19d4a

Browse files
committed
docs: G2 改为「接既有机制」并已实测验证;补第四条形态判据
三处更新。 1. G2 的方案换掉了,原因是调研发现生态里早就有一套完整设计。EGL 侧的形态是:一个共享 的 vendor 目录,mesa 和 nvidia-gl-host-link 各写一份 JSON 进去,靠 10_/50_ 文件名前缀 定优先级 —— 和发行版的约定一致。graphics.lua 的注释还记录了改成共享目录之前跨 vendor 优先级是「correct by alphabetical accident」。 Vulkan 侧缺的就是这一半:sentinel 已经把 libGLX_nvidia.so.0 软链进来(注释写着 "GLX vendor AND Vulkan ICD: same file")、已经供给 libX11/libXext、已经有写 ICD JSON 的代码路径,但只调了 declare_egl_vendor,没调 declare_vulkan_icd。于是 loader 退回去 扫 /usr/share,读到宿主那份裸 soname 的 JSON,缺 libXext 而放弃,再落到 lvp(llvmpipe) —— 静默变成软件渲染。 原提案的 VK_DRIVER_FILES 作废:那是在既有机制之外另起一套。 2. V1 在写任何代码之前先验过,非破坏性(不动 subos 共享目录,用 VK_DRIVER_FILES 指一份 等效 JSON): A 现状 devices = 1 → llvmpipe (LLVM 20.1.2) B V1 假设 devices = 1 → NVIDIA GeForce RTX 4080 差别只有「那份 JSON 在不在、路径是不是绝对的」。顺带发现第二处更小的泄漏:宿主的 Vulkan layer(libVkLayer_MESA_device_select)同样被扫到,不影响结果但值得单独清理。 3. 补第四条判据:上游源码能直接用的走 compat 内联,需要 fork 的才走 mcpp 原生模块化。 操作性问题是「解开 tarball 能不能只靠一份 mcpp.toml 编出来」。并写明模块层是选 B 的 结果而不是理由 —— 不要为了加模块层把 A 推成 B。 另:最小 host 面写成设计而不是妥协。sentinel(nvidia-gl-host-link / libcuda-host-link / wsl-gl-host-link)把宿主面收敛到「几个具名文件 + 一份 vendor 声明」,而且在没有该硬件的 机器上 install() 成功且什么都不链 —— 「"no NVIDIA on this machine" is a normal state」。 compat.vulkan-runtime 应归入这一类说清楚,而不是删掉。
1 parent 777987b commit 2f19d4a

1 file changed

Lines changed: 110 additions & 22 deletions

File tree

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

Lines changed: 110 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,32 @@ Date: 2026-08-30 · 前置:[`2026-08-30-gbm-cross-repo-closed-loop-plan.md`](202
1515
**判据不重新论证**:可独立分发 → 源码构建;目标相关的决定进 `build.mcpp`,能预先算定的
1616
生成物签进仓 + CI diff;模块名跟接口所有者。三条都在前一份文档里立过并验证过。
1717

18+
### 0.1 形态判据(本轮补齐的第四条)
19+
20+
前三条决定「怎么建」,这一条决定「放哪、叫什么」:
21+
22+
> **上游源码能直接用的 → `compat.*` 内联描述符;
23+
> 需要 fork 才能建的 → mcpp 原生模块化(Form A + 模块层),namespace 用上游组织名。**
24+
25+
| | 触发条件 | 形态 | 本轮的例子 |
26+
|---|---|---|---|
27+
| **A. `compat.*` 内联** | 上游 tarball 直接就能编:源码列得出来、没有必须先编译才能跑的生成器、没有 workspace | 描述符一个文件,`mcpp = { ... }` | `pixman`、`libxkbcommon`、`libevdev`、`mtdev` |
28+
| **B. fork + 原生模块化** | 需要 `build.mcpp`、需要 workspace 多成员、或有必须先编译出来才能跑的生成器 | Form A fork,`mcpp = "*/mcpp/<member>/mcpp.toml"`,带模块层 | GL 家族(libglvnd fork)、wayland-protocols(wayland fork) |
29+
30+
判 A 还是 B 的**操作性问题**:「把上游 tarball 解开,能不能只靠一份 `mcpp.toml` 把它编出来?」
31+
能就是 A,不能就是 B。B 的成本主要在维护一个 fork,所以**不要因为「顺便加个模块层」而
32+
把 A 推成 B** —— 模块层是 B 的**结果**,不是选 B 的理由。
33+
34+
按这条,本设计里 G1/G3 是 B(都进已有的 fork,新增仓 0),G4 全是 A。
35+
1836
---
1937

2038
## 1. 调研结论速览
2139

2240
| # | 目标 | 调研结论 | 规模 |
2341
|---|---|---|---|
2442
| **G1** | GL 家族(GLESv2 等) | ✅ **形态完全确定**,四张生成表已实跑通过 | 中,收益最大 |
25-
| **G2** | Vulkan 去宿主 | ⚠️ **原假设被推翻**,真问题不是 vulkan-runtime | 小,但有永久边界 |
43+
| **G2** | Vulkan 走生态驱动 | ⚠️ **原假设两次被推翻**,最终是「既有机制的 Vulkan 那一半没接」 | 一处调用,在 xim-pkgindex |
2644
| **G3** | wayland-protocols | ✅ 形态确定,可全量预生成 | 小 |
2745
| **G4** | libxkbcommon / pixman | 独立项目,内联描述符 | 中 |
2846
| **G5** | libinput | 拖 libevdev/mtdev/libudev | 中偏大 |
@@ -155,27 +173,97 @@ physical devices = 1
155173
/usr/share/vulkan/icd.d/{asahi,intel,lvp,nouveau,nvidia,radeon,…}_icd.json ← 宿主的
156174
```
157175

158-
### 3.3 结论:三件事,原来只看到一件
176+
### 3.3 根因:机制早就在,只是 Vulkan 这一半没接
159177

160-
1. **发现机制早就通了。** `mesa.lua` 已经调 `graphics.declare_vulkan_icd(...)`,
161-
生态的 ICD 通过 `XDG_DATA_DIRS` 被 loader 找到。这一条**不需要做任何事**。
162-
2. **`/usr/share` 也在 `XDG_DATA_DIRS` 里,于是宿主 ICD 一并可见。** 这比「找不到」更糟:
163-
部分能加载(llvmpipe),部分失败(NVIDIA 缺 libXext)——**结果是静默落到软件光栅化**,
164-
而不是干净地报错。这才是真正的问题,而且它**不在 mcpp-index 里**,在 xim-pkgindex
165-
的 discovery 层怎么拼 `XDG_DATA_DIRS`。
166-
3. **专有驱动是永久边界。** NVIDIA 的 Vulkan 驱动不可能进生态 payload。任何方案都要
167-
诚实地把「专有 GPU 走宿主」写出来,而不是假装能覆盖。
178+
生态里**已经有一套完整的「共享 vendor 目录」设计**,而且已经在 EGL 上跑通了。不需要
179+
新机制,需要的是把 Vulkan 接进同一套。
168180

169-
### 3.4 建议形态(按代价排序)
181+
`libs/graphics.lua` 与 `nvidia-gl-host-link.lua` 里的既有形态:
170182

171-
| 选项 | 做什么 | 代价 | 评价 |
172-
|---|---|---|---|
173-
| **V-a** | discovery 层用 **`VK_DRIVER_FILES`** 精确指向生态 ICD,不再依赖 `XDG_DATA_DIRS` 的目录合并 | xim-pkgindex 一行 | ✅ **推荐**。`VK_DRIVER_FILES` 是 Khronos loader 的显式列表,压过目录扫描,宿主 ICD 自然出局 |
174-
| V-b | 保留 `XDG_DATA_DIRS`,但把 `/usr/share` 从中摘掉 | 影响面大(XDG 不只服务 Vulkan) | ❌ 会伤到别的东西 |
175-
| V-c | 维持现状 + `compat.vulkan-runtime` 继续做宿主农场 | 0 | ⚠ 只对专有驱动有意义,应当**明确降级为「专有 GPU 专用」**并在描述符里写清楚 |
183+
```
184+
share/glvnd/egl_vendor.d/ ← 共享目录,谁都可以往里放
185+
10_nvidia.json ← nvidia-gl-host-link 写的(sentinel)
186+
50_mesa.json ← mesa 写的
187+
```
188+
189+
优先级**由文件名前缀决定**,和每个发行版的约定一样。`graphics.lua` 的注释写得很清楚:
190+
之前 mesa 和 nvidia 各自贡献**自己的**目录时,跨 vendor 优先级取决于 xlings 恰好把谁排
191+
在前面——「correct by alphabetical accident」。改成共享目录之后才有了确定的语义。
192+
193+
而 `nvidia-gl-host-link` 写 JSON 时用的是**绝对路径**,注释说明了为什么:
194+
195+
> a bare name here is a way for the host's stack to get back in through the door we closed
196+
197+
**Vulkan 侧缺的正是这一半。** 该 sentinel:
198+
199+
- ✅ 已经把 `libGLX_nvidia.so.0` 软链进自己的 `lib/`,注释还写着
200+
「GLX vendor **AND Vulkan ICD**: same file」
201+
- ✅ 已经供给 `libX11`/`libXext`/glibc/libglvnd(实测失败的正是 `libXext.so.6`)
202+
- ✅ 已经有写 ICD JSON 的现成代码路径
203+
- ❌ **只调了 `graphics.declare_egl_vendor(...)`,没有调 `declare_vulkan_icd(...)`**
204+
205+
于是 loader 找不到生态里的 NVIDIA ICD,退回去扫 `/usr/share`,读到宿主那份**裸 soname**
206+
的 `nvidia_icd.json`,dlopen 时缺 `libXext.so.6` 而失败,再往下落到宿主的
207+
`lvp_icd.json`(llvmpipe 依赖少,能加载)——**于是静默变成软件渲染**。
208+
209+
### 3.4 结论:一处改动,形态与 EGL 完全对称
210+
211+
| | 做什么 | 在哪个仓 |
212+
|---|---|---|
213+
| **V1** | `nvidia-gl-host-link` 增调 `graphics.declare_vulkan_icd(...)`,把 `10_nvidia.json`(绝对 `library_path`,指向它自己软链的 `libGLX_nvidia.so.0`)写进共享的 `share/vulkan/icd.d` | xim-pkgindex |
214+
| **V2** | `compat.vulkan-runtime` 描述符改写:说明它是**专有驱动的最小 host 面**,不是默认路径;生态驱动走 V1 | mcpp-index |
215+
216+
#### V1 已实测验证(2026-08-30,`default` subos,`--sandbox --gpu`)
217+
218+
提案在写任何代码之前先验过。非破坏性做法:不动 subos 的共享目录,用 `VK_DRIVER_FILES`
219+
指一份**等效**的 JSON —— `library_path` 写成 sentinel 那个软链的**绝对路径**:
220+
221+
```
222+
===== A. 现状(生态 + 宿主 /usr/share 混在一起)=====
223+
devices = 1
224+
- llvmpipe (LLVM 20.1.2, 256 bits) ← 软件渲染
225+
226+
===== B. V1 假设:NVIDIA ICD 用【绝对路径】声明 =====
227+
library_path = <subos>/lib/libGLX_nvidia.so.0
228+
devices = 1
229+
- NVIDIA GeForce RTX 4080 ← 真 GPU
230+
```
231+
232+
**差别只有「那份 JSON 在不在、路径是不是绝对的」。** sentinel 供给的
233+
`libX11`/`libXext` 让 NVIDIA 的驱动这次加载成功了,而现状下 loader 读的是宿主那份裸
234+
soname 的 JSON、缺 `libXext.so.6` 而放弃。所以 V1 不是推理,是已验证的结论。
235+
236+
**顺带发现的第二处、更小的泄漏**:`libVkLayer_MESA_device_select.so`(宿主的 Vulkan
237+
**layer**,不是驱动)同样从 `/usr/share` 被扫到并加载失败。它不影响结果(layer 缺失
238+
只是少一层),但说明 `XDG_DATA_DIRS` 里的 `/usr/share` 会同时把 layer 带进来。
239+
**layer 的清理是独立的一小步,不阻塞 V1。**
240+
241+
V1 之后共享目录里是 `10_nvidia.json`(sentinel)+ `50_mesa.json`(mesa),而 subos 的
242+
`share` 在 `XDG_DATA_DIRS` **最前面**(实测确认),所以生态的 ICD 先被扫到、`10_` 前缀
243+
让 NVIDIA 优先——**与 EGL 那一半逐字对称**。宿主的 `/usr/share` 仍在列表后面,但已经
244+
轮不到它。
245+
246+
~~原提案 `VK_DRIVER_FILES`~~ **作废**:那是在既有机制之外另起一套。既有的共享目录 +
247+
文件名优先级已经解决了同一个问题,而且已经过 EGL 验证。
248+
249+
### 3.5 最小 Host 面:这是设计,不是妥协
250+
251+
**不开源的驱动/运行时层**(NVIDIA 专有 GL/EGL/Vulkan、CUDA)不可能进生态 payload。
252+
生态对此的既有答案是 **sentinel + 共享目录**:
253+
254+
```
255+
nvidia-gl-host-link Sentinel: stable symlinks to the host's NVIDIA proprietary GL/EGL userspace
256+
libcuda-host-link 同上,CUDA
257+
wsl-gl-host-link 同上,WSL
258+
```
259+
260+
sentinel 的性质值得点明:它**只做符号链接与一份 JSON**,把宿主面收敛到
261+
「几个具名文件 + 一个 vendor 声明」,而不是让每个消费者各自去 `/usr/lib` 捞。
262+
`install()` 在没有 NVIDIA 的机器上**成功且什么都不链** —— 注释原话:
263+
「"no NVIDIA on this machine" is a normal state, not a failure」。
176264

177-
**V-a + V-c 并存**是最诚实的形态:生态驱动走 `VK_DRIVER_FILES` 干净闭环;专有驱动仍需
178-
`compat.vulkan-runtime`,且它的描述符要改写成「这是专有驱动的逃生舱,不是默认路径」。
265+
**所以设计原则是:host 面保留,但收敛到 sentinel,并且只对不开源的那一层。**
266+
V2 要做的就是把 `compat.vulkan-runtime` 归到这一类里说清楚,而不是删掉它。
179267

180268
---
181269

@@ -234,9 +322,9 @@ G1a glesv2/glesv1/opengl 三个成员(C 库) ← 无前置,规模最小,
234322
│
235323
├─ G1b khronos.glesv2 等模块层 ← 依赖 G1a;先验外部链接风险
236324
│
237-
G2a VK_DRIVER_FILES(xim-pkgindex) ← 无前置,与 G1 并行
325+
G2a nvidia-gl-host-link 增调 declare_vulkan_icd ← 无前置,与 G1 并行(xim-pkgindex)
238326
│
239-
└─ G2b compat.vulkan-runtime 降级为专有驱动逃生舱 ← 依赖 G2a
327+
└─ G2b compat.vulkan-runtime 归类为「专有驱动的最小 host 面」 ← 依赖 G2a
240328
241329
G3 wayland-protocols(wayland fork 第五成员) ← 无前置,与 G1/G2 并行
242330
G4a libxkbcommon + xkeyboard-config ← 无前置
@@ -251,7 +339,7 @@ G6 libudev 路线决策 → libseat ← 最后,单独评估
251339

252340
**排序建议**:`G1a → G3 → G2a → G4 → G1b → G2b → G5 → G6`
253341

254-
理由:G1a 是从「能初始化」到「能画」的分水岭;G3 之后才谈得上实现一个 shell;G2a 一行
342+
理由:G1a 是从「能初始化」到「能画」的分水岭;G3 之后才谈得上实现一个 shell;G2a 一处调用
255343
改动换掉一处静默降级;模块层(G1b)放在 C 库稳定之后,免得两个风险叠在一起。
256344

257345
---
@@ -278,7 +366,7 @@ G6 libudev 路线决策 → libseat ← 最后,单独评估
278366
| V1 | 三个 GL 库 soname 正确且**非空** | CI:`readelf -d` + 符号断言 + obj 计数(沿用 libglvnd 现有检查) |
279367
| V2 | GLESv2 与 EGL 用**同一个** GLdispatch | 测试里 `dladdr(&glClear)` 与 `dladdr(&eglInitialize)` 的 GLdispatch 路径一致 |
280368
| V3 | **拿到 context 之后真能画** | 沙箱 `--gpu`:GBM→EGL→`eglMakeCurrent`→`glClear`+`glReadPixels` 读回像素值 |
281-
| V4 | Vulkan 落到**真实** GPU 而非 llvmpipe | 沙箱:`vkEnumeratePhysicalDevices` 的 `deviceName` 不含 `llvmpipe`(在有 Mesa 支持的 GPU 上) |
369+
| V4 | Vulkan 落到**真实** GPU 而非 llvmpipe | ✅ **已验证**(见 §3.4):`deviceName` 从 `llvmpipe` 变成 `NVIDIA GeForce RTX 4080`。做成 V1 之后应固化为沙箱回归 |
282370
| V5 | 宿主一条都没赢 | 沿用 `tests/verify_graphics_closed_loop_sandbox.sh`,扩到 GL/Vulkan |
283371
| V6 | 生成表无漂移 | CI `generated` job 扩四行 diff |
284372

0 commit comments

Comments
 (0)