From 117d86bf3acd50eef1f9238e707fa80801657d9c Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Sun, 30 Aug 2026 13:50:56 +0800 Subject: [PATCH 01/17] =?UTF-8?q?docs:=20=E9=97=AD=E7=8E=AF=E5=B7=B2?= =?UTF-8?q?=E5=AF=B9=E5=B7=B2=E5=8F=91=E5=B8=83=E7=9A=84=20main=20?= =?UTF-8?q?=E5=A4=8D=E9=AA=8C,=C2=A719.6=20=E7=9A=84=E7=A0=B4=E5=9D=8F?= =?UTF-8?q?=E7=A1=AE=E8=AE=A4=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #293 合并后又跑了一遍沙箱验证,这次 BRANCH=main,并先清空沙箱里上一轮的 store —— 所以 是真从已发布索引取,不是复用分支那次的产物(日志里五个包都是 "Compiling" 而非 "Cached")。闭包与分支那次逐条一致,结果 PASS。 按 §18.2 自己立的规矩,对着已发布 artifact 的这一次才是权威的,所以放在完整输出之前。 同时复验并记录:main 上两个描述符的 sha256 与各自 tag 上的 tarball 一致(§19.6 那处 我先重切 tag 后改描述符造成的破坏已随合并修复),compat.egl.lua 返回 404。 §19.8 更新:#293 已合并;示例 09 的 PR #532 CI 20/20 通过。 --- ...6-08-30-gbm-cross-repo-closed-loop-plan.md | 39 +++++++++++++++++-- 1 file changed, 35 insertions(+), 4 deletions(-) diff --git a/.agents/docs/2026-08-30-gbm-cross-repo-closed-loop-plan.md b/.agents/docs/2026-08-30-gbm-cross-repo-closed-loop-plan.md index 05ce84a..5baa000 100644 --- a/.agents/docs/2026-08-30-gbm-cross-repo-closed-loop-plan.md +++ b/.agents/docs/2026-08-30-gbm-cross-repo-closed-loop-plan.md @@ -1214,6 +1214,37 @@ xlings subos use --sandbox --gpu \ 不在」—— 一个把 `/usr` 藏起来的沙箱对用户的机器什么也证明不了 —— 而是「宿主在、可达、 且依然全部落败」。后者才是真实机器上会发生的情形。 +#### 结论:闭环成立,已对**已发布的 main** 复验 + +#293 合并(`dc961cc`)之后又跑了一遍,这次 `BRANCH=main`,并且**先把沙箱里上一轮的 +store 清空**,确保是真从已发布索引取而不是复用分支那次的产物: + +``` +store cleared; now building from the PUBLISHED index + Compiling compat.libdrm v2.4.134 ← 没有 "Cached",全部重新下载编译 + Compiling compat.libgbm v25.0.7 + Compiling freedesktop.egl v1.7.0 + Compiling freedesktop.wayland v1.26.0 + Compiling freedesktop.wayland-server v1.26.0 + +PASS: the host's copies were present and reachable, and none of them won + EGL_VERSION 1.5 libglvnd + gbm_bo_create 256x256 stride=1024 + eglInitialize EGL 1.5, vendor Mesa Project +RESULT: PASS +``` + +闭包与下面分支那次逐条一致。**这一次才是权威的** —— 按 §18.2 自己立的规矩:对着已发布的 +artifact 验证,而不是本地打补丁的副本。 + +§19.6 记的那处自己造成的破坏也随合并修复,已复验: + +``` +main 的 freedesktop.wayland.lua sha256 == v1.26.0 tag 上 tarball 的 sha256 ✓ +main 的 freedesktop.egl.lua sha256 == v1.7.0 tag 上 tarball 的 sha256 ✓ +pkgs/c/compat.egl.lua HTTP 404(已删除) ✓ +``` + #### 完整输出(2026-08-30,`eco-gbm-20260830`,分支 `feat/freedesktop-egl` @ 2f4458d) ``` @@ -1363,10 +1394,10 @@ rm -rf ~/.mcpp/registry/data/xpkgs/freedesktop-x-wayland*/1.26.0 \ ### 19.8 仍未闭合 -- **PR #293 必须合并**才能修好 main 上 wayland 的 sha256(§19.6)。这是唯一真正阻塞的一条。 -- 示例 09 的更新已推到 mcpp PR #532。曾以为「推上去会把已绿的 CI 弄红」而扣着不推, - **那是假设、没查**:mcpp 的 CI 根本不构建 `examples/09-graphics-stack`(只有 - `openkal-cross.yml` 碰一个无关示例)。扣着不推让工作停在本地,比推上去更糟。 +- ~~PR #293 必须合并~~ **已合并**(`dc961cc`),§19.6 的破坏随之修复并已复验。 +- 示例 09 的更新已推到 mcpp PR #532(CI 20/20 通过)。曾以为「推上去会把已绿的 CI 弄红」 + 而扣着不推,**那是假设、没查**:mcpp 的 CI 根本不构建 `examples/09-graphics-stack` + (只有 `openkal-cross.yml` 碰一个无关示例)。扣着不推让工作停在本地,比推上去更糟。 - `libGL` / `libGLX` / `libGLESv{1,2}` 未构建。各自是 fork 里一个成员加一张 `mcpp/generated/` 里的 dispatch 表;索引里目前没有消费者。 - `freedesktop.egl` 的 `libEGL.so.1` 比 payload 的多三条 `DT_NEEDED` From c5ade981091f437cca1f8fbbec04530375ca0c9c Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Sun, 30 Aug 2026 14:21:54 +0800 Subject: [PATCH 02/17] =?UTF-8?q?docs:=20=C2=A720=20=E8=A6=86=E7=9B=96?= =?UTF-8?q?=E9=9D=A2=E7=9B=98=E7=82=B9=20=E2=80=94=E2=80=94=20=E3=80=8C?= =?UTF-8?q?=E8=83=BD=E8=B7=91=E9=80=9A=E3=80=8D=E4=B8=8D=E7=AD=89=E4=BA=8E?= =?UTF-8?q?=E3=80=8C=E5=A4=9F=E7=94=A8=E3=80=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit §19 证明了 GBM/DRM/EGL/Wayland 这条栈在沙箱里从零构建能跑通。但 #527 问的是能不能 满足系统级开发,而「跑通」和「够用」是两回事,之前没有把这个区分写下来。 三个场景逐个给结论: GBM 显存管理 ✅ 完全覆盖,已实测 Wayland 合成器 ⚠️ 只覆盖了协议库那一层 Mesa/Vulkan 扩展 ❌ Vulkan 侧仍伸手够宿主 缺口逐个点名而不是含糊带过。最要命的一个是 libGLESv2:合成器是拿到 EGL context 之后 用 GLES2 画的,而 libglvnd 的 GL/GLES 系列一个都没建 —— 也就是 EGL 能初始化,但拿到 context 之后没有 GL 可调。索引里 grep GLESv2 返回空。其余缺 wayland-protocols、 libxkbcommon、libinput、libudev/libseat、pixman。 Vulkan 那条 host 边(compat.vulkan-runtime 把宿主 /usr/lib 的 ICD 做成符号链接农场) 是当时有意为之的权宜,因为没有可绑的 payload。§17.2 已查明前提变了,现在可以照 compat.libgbm / freedesktop.egl 的同一形态拆掉:让环境声明,包自己什么都不设。 补齐的形态不需要再论证 —— 全部由这一轮已确立的判据(可独立分发 → 源码构建)直接推出。 排序 G1(GLES,补上渲染链)→ G5(拆掉最后一条 host 边)→ 其余。 同时更正 §19.8:mcpp 的示例 PR #532 已关闭且本就不需要合并,示例不是生态的一部分。 --- ...6-08-30-gbm-cross-repo-closed-loop-plan.md | 93 ++++++++++++++++++- 1 file changed, 89 insertions(+), 4 deletions(-) diff --git a/.agents/docs/2026-08-30-gbm-cross-repo-closed-loop-plan.md b/.agents/docs/2026-08-30-gbm-cross-repo-closed-loop-plan.md index 5baa000..f46b987 100644 --- a/.agents/docs/2026-08-30-gbm-cross-repo-closed-loop-plan.md +++ b/.agents/docs/2026-08-30-gbm-cross-repo-closed-loop-plan.md @@ -9,7 +9,8 @@ Date: 2026-08-30 · 起因:`compat.libgbm`(mcpp-index PR #281)· 状态:待 revi | 想知道 | 看哪节 | 注意 | |---|---|---| -| **最新一轮(EGL 源码化 + 模块命名)** | **§19** | 最权威;§19.6 记了一处自己造成的破坏 | +| **系统级开发到底覆盖到哪(#527 的答案)** | **§20** | 三个场景逐个给结论,缺口逐个点名 | +| **最新一轮(EGL 源码化 + 模块命名)** | **§19** | §19.6 记了一处自己造成的破坏 | | **沙箱干净房间验证** | **§19.4** | 宿主库在场且可达却全部落败 | | 模块该叫什么名字 | §19.2 | 跟接口的所有者,不跟发实现的人 | | 生成器放 build.mcpp 还是签进仓 | §19.3 | 判据是「依不依赖目标平台」 | @@ -1395,11 +1396,95 @@ rm -rf ~/.mcpp/registry/data/xpkgs/freedesktop-x-wayland*/1.26.0 \ ### 19.8 仍未闭合 - ~~PR #293 必须合并~~ **已合并**(`dc961cc`),§19.6 的破坏随之修复并已复验。 -- 示例 09 的更新已推到 mcpp PR #532(CI 20/20 通过)。曾以为「推上去会把已绿的 CI 弄红」 - 而扣着不推,**那是假设、没查**:mcpp 的 CI 根本不构建 `examples/09-graphics-stack` - (只有 `openkal-cross.yml` 碰一个无关示例)。扣着不推让工作停在本地,比推上去更糟。 +- mcpp 的示例 PR #532 **已关闭,不需要合并** —— 示例只是把用法写出来给人看,不是生态的 + 一部分。生态是否可用由索引 + 沙箱验证(§19.4)回答,与那个 PR 无关。 +- **真正剩下的不是收尾,是覆盖面**,见下面的 §20。 - `libGL` / `libGLX` / `libGLESv{1,2}` 未构建。各自是 fork 里一个成员加一张 `mcpp/generated/` 里的 dispatch 表;索引里目前没有消费者。 - `freedesktop.egl` 的 `libEGL.so.1` 比 payload 的多三条 `DT_NEEDED` (libstdc++/libm/libgcc_s),因为模块接口单元被编译进库里。`freedesktop.wayland` 完全 同形,是「模块层随库一起发」的既定结果,已写进描述符。 + +--- + +## 20. 覆盖面盘点:系统级开发,mcpp 推荐方式到底能做到哪 + +issue #527 问的是「在系统级开发(Wayland 合成器、Mesa/Vulkan 扩展、GBM 显存管理)中, +代码直接 `-lgbm` 链宿主库 —— 用 mcpp 推荐方式能不能满足」。§19 证明了栈能跑通, +但**「能跑通」不等于「够用」**。这一节按它点名的三个场景逐个给结论,并且把没覆盖的 +点名说清楚,不含糊。 + +### 20.1 三个场景的结论 + +| 场景 | 结论 | 依据 | +|---|---|---| +| **GBM 显存管理** | ✅ **完全覆盖** | §19.4 实测:`gbm_create_device` + `gbm_bo_create` 真分配 256x256,拿到驱动自己的 stride;宿主 `libgbm.so.1` 在场可达却没被选中 | +| **Wayland 合成器** | ⚠️ **只覆盖了协议库那一层** | 协议库(client/server/scanner/util)+ EGL + GBM + DRM 都有;但**渲染链和输入链整条缺**,见 20.2 | +| **Mesa/Vulkan 扩展** | ❌ **Vulkan 侧仍伸手够宿主** | `compat.vulkan-runtime` 明写着它把宿主 `/usr/lib` 的 ICD 做成符号链接农场,是这个栈里最后一处 host 边 | + +**所以对 #527 的诚实回答是**:`-lgbm` 那个具体诉求,推荐方式**完全能覆盖且已证明**; +但把「系统级开发」整体说成已覆盖,是不成立的。 + +### 20.2 Wayland 合成器缺什么(逐个点名) + +一个真实合成器(wlroots / weston 量级)需要的,索引里的实际情况: + +| 需要 | 索引里 | 性质 | +|---|---|---| +| `libwayland-server` / `-client` / `-scanner` | ✅ 有,源码构建 | 已完成 | +| EGL、GBM、libdrm | ✅ 有 | 已完成 | +| **`libGLESv2`** | ❌ **没有** | ⚠ **最要命的一个** | +| `wayland-protocols`(xdg-shell 等 XML) | ❌ 没有 | 没有它连基本桌面 shell 都实现不了 | +| `libxkbcommon` | ❌ 没有 | 键盘 | +| `libinput` | ❌ 没有 | 输入 | +| `libudev` / `libseat` | ❌ 没有 | 设备枚举、session / DRM master | +| `pixman` | ❌ 没有 | 软件合成路径 | + +**`libGLESv2` 为什么最要命**:合成器是通过 EGL 拿 context、然后用 **GLES2** 画的。 +这一轮建出了 `libEGL.so.1`,但 libglvnd 的 GL/GLES 系列(`libGL`、`libGLX`、`libOpenGL`、 +`libGLESv1`、`libGLESv2`)**一个都没建** —— 见 mcpplibs/libglvnd 的 `README.mcpp.md` +"Not built here"。也就是说:**EGL 能初始化,但拿到 context 之后没有 GL 可调**。 +索引里 `grep -rl GLESv2 pkgs/` 返回空。 + +### 20.3 Vulkan 那条 host 边 + +`compat.vulkan-runtime` 的注释自己写得很清楚: + +> The libraries are right there in `/usr/lib/x86_64-linux-gnu`. What cannot reach +> them is the process: an mcpp-built binary runs under mcpp's OWN glibc … +> `runtime.library_dirs` below puts a package-owned directory of symlinks on that +> path, which is precisely how `compat.glx-runtime` makes host OpenGL work + +它是**有意为之的权宜**,不是疏漏:当时没有可绑的生态 payload。但 §17.2 已经查明前提变了 —— +`xim:mesa` 的 payload 里就有 `lib/libvulkan_radeon.so` 和 +`share/vulkan/icd.d/*.json`,而且 `mesa.lua` 的 `config()` 里已经调了 +`graphics.declare_vulkan_icd(...)`。**所以这条 host 边现在是可以拆掉的**,做法与 +`compat.libgbm` 用 `GBM_BACKENDS_PATH`、`freedesktop.egl` 用 +`__EGL_VENDOR_LIBRARY_DIRS` 完全同构:让环境声明 `VK_ICD_FILENAMES` / `VK_DRIVER_FILES`, +包自己什么都不设。 + +### 20.4 补齐的形态与排序(判据都已确立,不需要再论证) + +每一条的形态都能从这一轮已经确立的判据直接推出来 —— 全部是「可独立分发」→ 源码构建。 + +| # | 补什么 | 形态 | 规模 | +|---|---|---|---| +| **G1** | `libGLESv2` / `libGL` / `libGLX` | **现有 fork 加成员**:mcpplibs/libglvnd 已经有 `mcpp/generated/` 与 dispatch 机制,每个库就是一个成员 + 一张生成表 | 小,收益最大 —— 它补上渲染链 | +| **G2** | `wayland-protocols` | 纯 XML,`wayland-scanner` 已经有了;可以是 mcpplibs/wayland 的第五个成员或独立条目 | 小 | +| **G3** | `libxkbcommon`、`pixman` | 独立项目,内联描述符即可(与 `compat.libdrm` 同形) | 中 | +| **G4** | `libinput` | 独立项目,但拖 `libevdev` / `mtdev` / `libudev` | 中偏大 | +| **G5** | `compat.vulkan-runtime` 去掉 host 边 | 照 §17.2,走 `xim:mesa` 的 ICD 声明 | 小 | +| **G6** | `libudev` / `libseat` | systemd 邻域,最难;可先用 logind-less 路径绕开 | 大 | + +**排序建议:G1 → G5 → G2 → G3 → G4 → G6。** +G1 之后「EGL + GLES 渲染」这条链完整,是从「能初始化」到「能画」的分水岭; +G5 之后整个图形栈**零 host 边**(现在只剩它一处)。 + +### 20.5 一句话 + +> **GBM/DRM/EGL/Wayland-协议 这一层,mcpp 推荐方式已经完整覆盖并已实测; +> 但要做一个能跑的合成器,还差渲染链(GLES)和输入链;要做 Vulkan, +> 还有一处宿主依赖没拆。** + +把「能不能用」答成「能」是不诚实的,答成「不能」也不对。准确的说法是: +**这一轮把可行性证明了,覆盖面还没到。** 缺口已经点名、形态已经确定、排序已经给出。 From 777987b9b77d8ee01bb3c5c0714fa7cc6fd727c3 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Sun, 30 Aug 2026 14:32:22 +0800 Subject: [PATCH 03/17] =?UTF-8?q?docs:=20=E5=9B=BE=E5=BD=A2=E6=A0=88?= =?UTF-8?q?=E8=A6=86=E7=9B=96=E9=9D=A2=E8=AE=BE=E8=AE=A1=E6=96=B9=E6=A1=88?= =?UTF-8?q?(=E6=B7=B1=E5=BA=A6=E8=B0=83=E7=A0=94=E7=89=88)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 回答 §20 提出但没有展开的那个问题:从「能跑通」到「能写一个合成器」,具体要做什么。 每一条都调研到能直接动手,不是列愿望: G1 读了 libglvnd 的 meson,拿到五个库的精确构成,并发现一个决定规模的分层 —— GLESv2/GLESv1/OpenGL 不需要 X11,只有 GLX/GL 需要。所以只做前三个就覆盖合成器的全部 需要。四张生成表已经实跑通过(gl 78373 行 / opengl 30197 / glesv2 15788 / glesv1 11292)。 也读出了「为什么必须拆成员」的硬理由:stub.c 和 entry 文件被用两套不同的宏编译两次, 一个 mcpp 包做不到。 G2 的原假设被实测推翻。沙箱里不装 vulkan-runtime,Vulkan 其实已经能跑 —— 但落到 llvmpipe,因为 /usr/share 也在 XDG_DATA_DIRS 里,宿主 ICD 一并可见、部分能加载。真正 的问题不是 compat.vulkan-runtime,是 discovery 层怎么拼 XDG_DATA_DIRS;建议改用 VK_DRIVER_FILES 精确指定。另外专有驱动是永久边界,不该假装能覆盖。 G3 wayland-protocols 判据指向全量预生成 + 签进仓,做成 wayland fork 的第五个成员而不是 新建仓。G4 里 pixman 的 SIMD 自带门控,不需要 build.mcpp —— 与 GLdispatch 的 entry stub 正相反,是判据的一个好对照。 附任务依赖图(三条可并行)、八角度评估、六条验证矩阵,以及明确不做的四件事。 核心验证是 V3:glClear + glReadPixels 读回像素,那才是「能画」的定义。 --- ...26-08-30-graphics-stack-coverage-design.md | 304 ++++++++++++++++++ 1 file changed, 304 insertions(+) create mode 100644 .agents/docs/2026-08-30-graphics-stack-coverage-design.md diff --git a/.agents/docs/2026-08-30-graphics-stack-coverage-design.md b/.agents/docs/2026-08-30-graphics-stack-coverage-design.md new file mode 100644 index 0000000..3fc30d4 --- /dev/null +++ b/.agents/docs/2026-08-30-graphics-stack-coverage-design.md @@ -0,0 +1,304 @@ +# mcpp 图形栈:从「能跑通」到「能开发」的覆盖面设计 + +Date: 2026-08-30 · 前置:[`2026-08-30-gbm-cross-repo-closed-loop-plan.md`](2026-08-30-gbm-cross-repo-closed-loop-plan.md) §19/§20 · 状态:待 review + +## 0. 这份文档解决什么 + +前一份文档证明了 GBM/DRM/EGL/Wayland-协议 这条栈**能跑通**(沙箱干净房间,宿主库在场 +可达却全部落败)。但 §20 的盘点显示,「跑通」离「能写一个合成器」还差两条链: + +- **渲染链**:EGL 能初始化,但拿到 context 之后**没有 GL 可调** +- **输入链**:整条缺 + +以及 Vulkan 那一处宿主依赖。这份文档把每一条**调研到能直接动手的程度**,而不是列愿望。 + +**判据不重新论证**:可独立分发 → 源码构建;目标相关的决定进 `build.mcpp`,能预先算定的 +生成物签进仓 + CI diff;模块名跟接口所有者。三条都在前一份文档里立过并验证过。 + +--- + +## 1. 调研结论速览 + +| # | 目标 | 调研结论 | 规模 | +|---|---|---|---| +| **G1** | GL 家族(GLESv2 等) | ✅ **形态完全确定**,四张生成表已实跑通过 | 中,收益最大 | +| **G2** | Vulkan 去宿主 | ⚠️ **原假设被推翻**,真问题不是 vulkan-runtime | 小,但有永久边界 | +| **G3** | wayland-protocols | ✅ 形态确定,可全量预生成 | 小 | +| **G4** | libxkbcommon / pixman | 独立项目,内联描述符 | 中 | +| **G5** | libinput | 拖 libevdev/mtdev/libudev | 中偏大 | +| **G6** | libudev / libseat | systemd 邻域,最难 | 大 | + +--- + +## 2. G1 — libglvnd 的 GL 家族 + +### 2.1 五个库的精确构成(读 meson 得到,非推测) + +`src/GLdispatch/vnd-glapi/meson.build` 有一个 `foreach`,为四种 API 风味各建一个静态库: + +```meson +foreach g : ['gl', 'opengl', 'glesv1', 'glesv2'] + _lib = static_library( + 'glapi_' + g, + ['stub.c', _entry_files, header], # header = g_glapi_mapi__tmp.h + c_args : ['-DSTATIC_DISPATCH_ONLY', + '-DMAPI_ABI_HEADER="…"'], + ) +endforeach +``` + +然后各个共享库: + +| 库 | soname | 构成 | X11? | +|---|---|---|---| +| `libGLESv2` | `libGLESv2.so.2` | `libopengl.c` + `idep_glapi_glesv2` + gldispatch | **否** | +| `libGLESv1` | `libGLESv1_CM.so.1` | `libopengl.c` + `idep_glapi_glesv1` + gldispatch | **否** | +| `libOpenGL` | `libOpenGL.so.0` | `libopengl.c` + `idep_glapi_opengl` + gldispatch | **否** | +| `libGLX` | `libGLX.so.0` | `libglx.c` `libglxmapping.c` `libglxproto.c` `glvnd_genentry.c` + 生成的 stub list | **是**(`dep_x11` `dep_xext` `dep_glproto`) | +| `libGL` | `libGL.so.1` | `libgl.c` + `idep_glapi_gl` + **`dep_glx`** | **是**(经 libGLX) | + +**这个分层是本节最有用的发现**:三个无 X11 的库覆盖了合成器的全部需要;`libGLX`/`libGL` +只有 X11 应用需要,而索引里 `compat.x11` / `compat.xext` / `compat.xorgproto` 已经齐备。 +**所以 G1 可以只做前三个**,规模立刻小一半,而且不引入 X11 依赖。 + +### 2.2 四张生成表 —— 已实跑验证 + +命令从 `src/generate/meson.build` 逐字转写,2026-08-30 实跑: + +```bash +cd upstream/src/generate +python3 gen_gldispatch_mapi.py opengl xml/gl.xml xml/gl_other.xml > g_glapi_mapi_opengl_tmp.h # 30197 行 ✓ +python3 gen_gldispatch_mapi.py glesv1 xml/gl.xml xml/gl_other.xml > g_glapi_mapi_glesv1_tmp.h # 11292 行 ✓ +python3 gen_gldispatch_mapi.py glesv2 xml/gl.xml xml/gl_other.xml > g_glapi_mapi_glesv2_tmp.h # 15788 行 ✓ +# libGL 特殊:符号表从文件读,不从 XML 生成 +python3 gen_gldispatch_mapi.py ../GL/gl.symbols xml/gl.xml xml/gl_other.xml \ + > g_glapi_mapi_gl_tmp.h # 78373 行 ✓ +``` + +四条全部成功。**都签进 `mcpp/generated/`**,与已有的 `glapi_mapi_tmp.h` 并列,CI 的 +`generated` job 加四行 diff 即可 —— 构建期依然不跑 Python。 + +### 2.3 为什么必须是独立成员,而不是给 gldispatch 加 target + +`stub.c` 和那组 per-arch entry 文件**被编译两次,用不同的宏**: + +| | 宏 | +|---|---| +| `libglapi`(在 gldispatch 里) | `-DMAPI_ABI_HEADER="glapi_mapi_tmp.h"` | +| `glapi_`(每个 GL 库) | `-DSTATIC_DISPATCH_ONLY -DMAPI_ABI_HEADER="g_glapi_mapi__tmp.h"` | + +mcpp 把一个包的源码**编一次**、让每个库 target 链接全部对象。同一个 `.c` 需要两套宏, +一个包做不到 —— 这与 `freedesktop.wayland` 拆四个成员是同一条约束,mcpp 自己的告警 +也点了这个办法(「split into a workspace member」)。 + +**结论**:`mcpp/glesv2`、`mcpp/glesv1`、`mcpp/opengl` 各是一个成员,各自 path 依赖 +`mcpp/gldispatch`,与 `mcpp/egl` 平级。 + +### 2.4 源码构建 vs 绑定 payload —— 为什么仍然是源码 + +`xim:libglvnd` 的 payload **确实带** `libGLESv2.so.2`(实测,连 libGL/libGLX/libOpenGL +都有)。所以这里存在真实的选择,不是只有一条路。 + +判据本身已经给出答案(libglvnd 可独立分发 → 源码)。但还有一条**具体**理由: + +``` +payload libGLESv2.so.2 → DT_NEEDED: libGLdispatch.so.0 (实测) +``` + +如果绑 payload 的 GLESv2、同时用我们源码构建的 `freedesktop.egl`,那么 payload 的 +GLESv2(glvnd **1.7.0.1**)会因为 soname 复用去用我们建的 GLdispatch(glvnd **1.7.0**)。 +这**能跑**(soname 复用已实测),但它是一处跨构建耦合,靠 +`__glDispatchGetABIVersion` 在运行期兜底。源码构建让整个 dispatch 家族**同一份构建、 +同一个版本**,这个耦合根本不存在。 + +### 2.5 模块层 + +按已立的规则,模块名跟接口所有者 —— GLES 与 OpenGL 都是 **Khronos** 的规范: + +``` +import khronos.glesv2; // 与 khronos.egl 并列 +import khronos.glesv1; +import khronos.opengl; +``` + +⚠ **一个必须先验证的风险**:GL 的入口点是否都是外部链接。`khronos.egl` 不需要 +forwarder 是因为 EGL 入口是 `EGLAPI … EGLAPIENTRY`;GL 的头(`GLES2/gl2.h`)用的是 +`GL_APICALL … GL_APIENTRY`,展开后是否同样是外部链接**需要先验一次**,否则会重演 +wayland 那次 clang 报 `using declaration referring to … with internal linkage cannot be +exported`。**建议:先只做 C 库三个成员,模块层作为独立一步。** + +--- + +## 3. G2 — Vulkan:调研推翻了原来的判断 + +### 3.1 原假设 + +§20 写的是「`compat.vulkan-runtime` 把宿主 `/usr/lib` 的 ICD 做成符号链接农场,是最后 +一处 host 边;照 §17.2 走 `xim:mesa` 的 ICD 声明就能拆掉」。 + +### 3.2 实测(沙箱,**不装** vulkan-runtime,只用 `compat.vulkan` 源码构建 loader) + +``` +vkCreateInstance = 0 (ok) +physical devices = 1 + - llvmpipe (LLVM 20.1.2, 256 bits) + +[Vulkan Loader] ERROR: libXext.so.6: cannot open shared object file +[Vulkan Loader] ERROR | DRIVER: loader_icd_scan: Failed loading library associated with + ICD JSON libGLX_nvidia.so.0. Ignoring this JSON +``` + +`XDG_DATA_DIRS` 的实际内容: + +``` +/share/vulkan/icd.d/radeon_icd.x86_64.json ← 生态的(xim:mesa) +/usr/share/vulkan/icd.d/{asahi,intel,lvp,nouveau,nvidia,radeon,…}_icd.json ← 宿主的 +``` + +### 3.3 结论:三件事,原来只看到一件 + +1. **发现机制早就通了。** `mesa.lua` 已经调 `graphics.declare_vulkan_icd(...)`, + 生态的 ICD 通过 `XDG_DATA_DIRS` 被 loader 找到。这一条**不需要做任何事**。 +2. **`/usr/share` 也在 `XDG_DATA_DIRS` 里,于是宿主 ICD 一并可见。** 这比「找不到」更糟: + 部分能加载(llvmpipe),部分失败(NVIDIA 缺 libXext)——**结果是静默落到软件光栅化**, + 而不是干净地报错。这才是真正的问题,而且它**不在 mcpp-index 里**,在 xim-pkgindex + 的 discovery 层怎么拼 `XDG_DATA_DIRS`。 +3. **专有驱动是永久边界。** NVIDIA 的 Vulkan 驱动不可能进生态 payload。任何方案都要 + 诚实地把「专有 GPU 走宿主」写出来,而不是假装能覆盖。 + +### 3.4 建议形态(按代价排序) + +| 选项 | 做什么 | 代价 | 评价 | +|---|---|---|---| +| **V-a** | discovery 层用 **`VK_DRIVER_FILES`** 精确指向生态 ICD,不再依赖 `XDG_DATA_DIRS` 的目录合并 | xim-pkgindex 一行 | ✅ **推荐**。`VK_DRIVER_FILES` 是 Khronos loader 的显式列表,压过目录扫描,宿主 ICD 自然出局 | +| V-b | 保留 `XDG_DATA_DIRS`,但把 `/usr/share` 从中摘掉 | 影响面大(XDG 不只服务 Vulkan) | ❌ 会伤到别的东西 | +| V-c | 维持现状 + `compat.vulkan-runtime` 继续做宿主农场 | 0 | ⚠ 只对专有驱动有意义,应当**明确降级为「专有 GPU 专用」**并在描述符里写清楚 | + +**V-a + V-c 并存**是最诚实的形态:生态驱动走 `VK_DRIVER_FILES` 干净闭环;专有驱动仍需 +`compat.vulkan-runtime`,且它的描述符要改写成「这是专有驱动的逃生舱,不是默认路径」。 + +--- + +## 4. G3–G6 — 合成器其余依赖 + +### 4.1 wayland-protocols 1.49 + +**纯 XML,没有代码。** 消费者需要对 `xdg-shell.xml` 等跑 `wayland-scanner` 生成 +`.h`/`.c`。索引已经有 `freedesktop.wayland-scanner`。 + +两种形态,判据已经给出答案: + +| | 做法 | 判据 | +|---|---|---| +| ❌ 消费者构建期生成 | 包只发 XML,消费者用 `build.mcpp` + `dep_bin(...)` 调 scanner | 输出**与目标平台无关** → 不该放构建期 | +| ✅ **全量预生成** | fork 里对每个协议跑 scanner,产物签进仓,CI diff | 与 `freedesktop.wayland` 的协议代码同一形态 | + +**建议**:作为 `mcpplibs/wayland` 的**第五个成员**(`mcpp/protocols`),而不是新建仓 —— +scanner 就在同一个 workspace 里,`mcpp/generated/` 机制现成。索引条目 +`freedesktop.wayland-protocols = "1.49"`。 + +⚠ 需要实测的一点:全量生成后的体积。约 40 个协议,预计 3–5 万行,可接受但要量过再定。 + +### 4.2 libxkbcommon 1.13.2 / pixman 0.46.4 + +两个都是独立项目,**内联描述符即可**(与 `compat.libdrm` 同形)。 + +- **libxkbcommon**:上游活跃仓在 GitHub(`xkbcommon/libxkbcommon`),freedesktop 那个 + 停在 0.3.0 —— 别取错。运行期需要 `xkeyboard-config` 的数据文件,这是**第二个包**, + 且是纯数据。 +- **pixman**:有 per-arch SIMD(SSE2/AVX2/NEON/…),各自独立 TU 且**带 `#ifdef` 自门控** + (与 libffi 同形),所以**不需要 `build.mcpp`** —— 全列进 `sources` 让它们自己关掉即可。 + 这一点与 GLdispatch 的 entry stub 正相反,是判据的一个好对照。 + +### 4.3 libinput 1.31.3 + +拖 `libevdev` 1.13.7、`mtdev`、以及 **`libudev`**。前两个是普通独立项目;`libudev` 是难点, +见下。 + +### 4.4 libudev / libseat — 真正的难点 + +- **libudev** 是 systemd 的一部分。可选替代:`eudev`(独立 fork,已停更)或 + `libudev-zero`(极简重实现)。**需要单独决策,不要顺手做。** +- **libseat**:`seatd` 0.9.3 提供,支持无 logind 的 `seatd` 后端 —— 合成器可以只用 + seatd 后端,绕开 systemd。 + +**建议**:这一层**先不做**。合成器可以在「已有 DRM master」的前提下开发(如从 TTY 直接 +启动、或 `SEATD_SOCK`),把 session 管理留到最后。 + +--- + +## 5. 任务依赖图与排序 + +``` +G1a glesv2/glesv1/opengl 三个成员(C 库) ← 无前置,规模最小,解锁渲染链 + │ + ├─ G1b khronos.glesv2 等模块层 ← 依赖 G1a;先验外部链接风险 + │ +G2a VK_DRIVER_FILES(xim-pkgindex) ← 无前置,与 G1 并行 + │ + └─ G2b compat.vulkan-runtime 降级为专有驱动逃生舱 ← 依赖 G2a + +G3 wayland-protocols(wayland fork 第五成员) ← 无前置,与 G1/G2 并行 +G4a libxkbcommon + xkeyboard-config ← 无前置 +G4b pixman ← 无前置 + │ +G5 libinput(+ libevdev + mtdev) ← 依赖 G6 的 libudev 决策 + │ +G6 libudev 路线决策 → libseat ← 最后,单独评估 +``` + +**可并行的三条**:G1a / G2a / G3。它们互不依赖,而且各自独立可验证。 + +**排序建议**:`G1a → G3 → G2a → G4 → G1b → G2b → G5 → G6` + +理由:G1a 是从「能初始化」到「能画」的分水岭;G3 之后才谈得上实现一个 shell;G2a 一行 +改动换掉一处静默降级;模块层(G1b)放在 C 库稳定之后,免得两个风险叠在一起。 + +--- + +## 6. 多角度评估 + +| 角度 | 评估 | +|---|---| +| **架构** | G1 不新建仓,复用 mcpplibs/libglvnd 已有的 `mcpp/generated/` 与 path 依赖机制;G3 复用 mcpplibs/wayland 的 scanner。**新增仓数量 = 0** | +| **稳定性** | 四张生成表已实跑;CI 的 diff 守卫扩四行即可。风险集中在 G1b(模块导出)与 G3(体积),两者都可先验 | +| **优雅/简洁** | 只做三个无 X11 的 GL 库,不碰 GLX/GL —— 覆盖合成器的全部需要,且不把 X11 拖进任何消费者 | +| **用户体验** | 合成器作者的 `mcpp.toml` 从「缺渲染链」变成可写;`import khronos.glesv2;` 与既有命名一致 | +| **兼容性** | 全部是新增条目,不动任何已发布包。唯一的行为变更是 G2a,而它修的是「静默落到软件渲染」 | +| **跨平台** | GL 家族的 per-arch entry stub 沿用 `build.mcpp` 里已有的 `target_arch()` 选择,aarch64/ppc64 由构造成立;pixman 的 SIMD 自门控,不需要额外机制 | +| **一致性** | 模块命名(khronos.*)、生成物签进仓、path 依赖三条规则原样复用,不引入新范式 | +| **无感升级** | 全是新增包,无同版本重切 tag 的问题(那正是上一轮的教训) | + +--- + +## 7. 验证矩阵 + +| # | 断言 | 怎么验 | +|---|---|---| +| V1 | 三个 GL 库 soname 正确且**非空** | CI:`readelf -d` + 符号断言 + obj 计数(沿用 libglvnd 现有检查) | +| V2 | GLESv2 与 EGL 用**同一个** GLdispatch | 测试里 `dladdr(&glClear)` 与 `dladdr(&eglInitialize)` 的 GLdispatch 路径一致 | +| V3 | **拿到 context 之后真能画** | 沙箱 `--gpu`:GBM→EGL→`eglMakeCurrent`→`glClear`+`glReadPixels` 读回像素值 | +| V4 | Vulkan 落到**真实** GPU 而非 llvmpipe | 沙箱:`vkEnumeratePhysicalDevices` 的 `deviceName` 不含 `llvmpipe`(在有 Mesa 支持的 GPU 上) | +| V5 | 宿主一条都没赢 | 沿用 `tests/verify_graphics_closed_loop_sandbox.sh`,扩到 GL/Vulkan | +| V6 | 生成表无漂移 | CI `generated` job 扩四行 diff | + +**V3 是这一轮的核心验证** —— 它是「能画」的定义。`glReadPixels` 读回一个已知颜色, +比任何符号存在性检查都硬。 + +--- + +## 8. 明确不做的事 + +- **libGLX / libGL**:合成器不需要,而且会把 X11 拖进依赖图。X11 应用要用时再做, + 索引里 `compat.x11`/`compat.xext`/`compat.xorgproto` 已经齐备。 +- **HGL**(Haiku 的 libGL):平台不相关。 +- **把 NVIDIA 专有驱动纳入生态**:不可能,也不该假装。 +- **libudev 的重实现**:超出图形栈范围,单独评估。 + +--- + +## 9. 一句话 + +> **G1a 三个成员是分水岭 —— 做完它,mcpp 从「EGL 能初始化」变成「能画」; +> 其余都是补齐,唯一的架构性问题是 Vulkan 的 `XDG_DATA_DIRS` 目录合并, +> 而那一行改在 xim-pkgindex,不在这里。** From 2f19d4ab87bf141d22a7ffbaa39bfff084f90f9a Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Sun, 30 Aug 2026 14:56:06 +0800 Subject: [PATCH 04/17] =?UTF-8?q?docs:=20G2=20=E6=94=B9=E4=B8=BA=E3=80=8C?= =?UTF-8?q?=E6=8E=A5=E6=97=A2=E6=9C=89=E6=9C=BA=E5=88=B6=E3=80=8D=E5=B9=B6?= =?UTF-8?q?=E5=B7=B2=E5=AE=9E=E6=B5=8B=E9=AA=8C=E8=AF=81;=E8=A1=A5?= =?UTF-8?q?=E7=AC=AC=E5=9B=9B=E6=9D=A1=E5=BD=A2=E6=80=81=E5=88=A4=E6=8D=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 三处更新。 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 应归入这一类说清楚,而不是删掉。 --- ...26-08-30-graphics-stack-coverage-design.md | 132 +++++++++++++++--- 1 file changed, 110 insertions(+), 22 deletions(-) diff --git a/.agents/docs/2026-08-30-graphics-stack-coverage-design.md b/.agents/docs/2026-08-30-graphics-stack-coverage-design.md index 3fc30d4..cb95653 100644 --- a/.agents/docs/2026-08-30-graphics-stack-coverage-design.md +++ b/.agents/docs/2026-08-30-graphics-stack-coverage-design.md @@ -15,6 +15,24 @@ Date: 2026-08-30 · 前置:[`2026-08-30-gbm-cross-repo-closed-loop-plan.md`](202 **判据不重新论证**:可独立分发 → 源码构建;目标相关的决定进 `build.mcpp`,能预先算定的 生成物签进仓 + CI diff;模块名跟接口所有者。三条都在前一份文档里立过并验证过。 +### 0.1 形态判据(本轮补齐的第四条) + +前三条决定「怎么建」,这一条决定「放哪、叫什么」: + +> **上游源码能直接用的 → `compat.*` 内联描述符; +> 需要 fork 才能建的 → mcpp 原生模块化(Form A + 模块层),namespace 用上游组织名。** + +| | 触发条件 | 形态 | 本轮的例子 | +|---|---|---|---| +| **A. `compat.*` 内联** | 上游 tarball 直接就能编:源码列得出来、没有必须先编译才能跑的生成器、没有 workspace | 描述符一个文件,`mcpp = { ... }` | `pixman`、`libxkbcommon`、`libevdev`、`mtdev` | +| **B. fork + 原生模块化** | 需要 `build.mcpp`、需要 workspace 多成员、或有必须先编译出来才能跑的生成器 | Form A fork,`mcpp = "*/mcpp//mcpp.toml"`,带模块层 | GL 家族(libglvnd fork)、wayland-protocols(wayland fork) | + +判 A 还是 B 的**操作性问题**:「把上游 tarball 解开,能不能只靠一份 `mcpp.toml` 把它编出来?」 +能就是 A,不能就是 B。B 的成本主要在维护一个 fork,所以**不要因为「顺便加个模块层」而 +把 A 推成 B** —— 模块层是 B 的**结果**,不是选 B 的理由。 + +按这条,本设计里 G1/G3 是 B(都进已有的 fork,新增仓 0),G4 全是 A。 + --- ## 1. 调研结论速览 @@ -22,7 +40,7 @@ Date: 2026-08-30 · 前置:[`2026-08-30-gbm-cross-repo-closed-loop-plan.md`](202 | # | 目标 | 调研结论 | 规模 | |---|---|---|---| | **G1** | GL 家族(GLESv2 等) | ✅ **形态完全确定**,四张生成表已实跑通过 | 中,收益最大 | -| **G2** | Vulkan 去宿主 | ⚠️ **原假设被推翻**,真问题不是 vulkan-runtime | 小,但有永久边界 | +| **G2** | Vulkan 走生态驱动 | ⚠️ **原假设两次被推翻**,最终是「既有机制的 Vulkan 那一半没接」 | 一处调用,在 xim-pkgindex | | **G3** | wayland-protocols | ✅ 形态确定,可全量预生成 | 小 | | **G4** | libxkbcommon / pixman | 独立项目,内联描述符 | 中 | | **G5** | libinput | 拖 libevdev/mtdev/libudev | 中偏大 | @@ -155,27 +173,97 @@ physical devices = 1 /usr/share/vulkan/icd.d/{asahi,intel,lvp,nouveau,nvidia,radeon,…}_icd.json ← 宿主的 ``` -### 3.3 结论:三件事,原来只看到一件 +### 3.3 根因:机制早就在,只是 Vulkan 这一半没接 -1. **发现机制早就通了。** `mesa.lua` 已经调 `graphics.declare_vulkan_icd(...)`, - 生态的 ICD 通过 `XDG_DATA_DIRS` 被 loader 找到。这一条**不需要做任何事**。 -2. **`/usr/share` 也在 `XDG_DATA_DIRS` 里,于是宿主 ICD 一并可见。** 这比「找不到」更糟: - 部分能加载(llvmpipe),部分失败(NVIDIA 缺 libXext)——**结果是静默落到软件光栅化**, - 而不是干净地报错。这才是真正的问题,而且它**不在 mcpp-index 里**,在 xim-pkgindex - 的 discovery 层怎么拼 `XDG_DATA_DIRS`。 -3. **专有驱动是永久边界。** NVIDIA 的 Vulkan 驱动不可能进生态 payload。任何方案都要 - 诚实地把「专有 GPU 走宿主」写出来,而不是假装能覆盖。 +生态里**已经有一套完整的「共享 vendor 目录」设计**,而且已经在 EGL 上跑通了。不需要 +新机制,需要的是把 Vulkan 接进同一套。 -### 3.4 建议形态(按代价排序) +`libs/graphics.lua` 与 `nvidia-gl-host-link.lua` 里的既有形态: -| 选项 | 做什么 | 代价 | 评价 | -|---|---|---|---| -| **V-a** | discovery 层用 **`VK_DRIVER_FILES`** 精确指向生态 ICD,不再依赖 `XDG_DATA_DIRS` 的目录合并 | xim-pkgindex 一行 | ✅ **推荐**。`VK_DRIVER_FILES` 是 Khronos loader 的显式列表,压过目录扫描,宿主 ICD 自然出局 | -| V-b | 保留 `XDG_DATA_DIRS`,但把 `/usr/share` 从中摘掉 | 影响面大(XDG 不只服务 Vulkan) | ❌ 会伤到别的东西 | -| V-c | 维持现状 + `compat.vulkan-runtime` 继续做宿主农场 | 0 | ⚠ 只对专有驱动有意义,应当**明确降级为「专有 GPU 专用」**并在描述符里写清楚 | +``` +share/glvnd/egl_vendor.d/ ← 共享目录,谁都可以往里放 + 10_nvidia.json ← nvidia-gl-host-link 写的(sentinel) + 50_mesa.json ← mesa 写的 +``` + +优先级**由文件名前缀决定**,和每个发行版的约定一样。`graphics.lua` 的注释写得很清楚: +之前 mesa 和 nvidia 各自贡献**自己的**目录时,跨 vendor 优先级取决于 xlings 恰好把谁排 +在前面——「correct by alphabetical accident」。改成共享目录之后才有了确定的语义。 + +而 `nvidia-gl-host-link` 写 JSON 时用的是**绝对路径**,注释说明了为什么: + +> a bare name here is a way for the host's stack to get back in through the door we closed + +**Vulkan 侧缺的正是这一半。** 该 sentinel: + +- ✅ 已经把 `libGLX_nvidia.so.0` 软链进自己的 `lib/`,注释还写着 + 「GLX vendor **AND Vulkan ICD**: same file」 +- ✅ 已经供给 `libX11`/`libXext`/glibc/libglvnd(实测失败的正是 `libXext.so.6`) +- ✅ 已经有写 ICD JSON 的现成代码路径 +- ❌ **只调了 `graphics.declare_egl_vendor(...)`,没有调 `declare_vulkan_icd(...)`** + +于是 loader 找不到生态里的 NVIDIA ICD,退回去扫 `/usr/share`,读到宿主那份**裸 soname** +的 `nvidia_icd.json`,dlopen 时缺 `libXext.so.6` 而失败,再往下落到宿主的 +`lvp_icd.json`(llvmpipe 依赖少,能加载)——**于是静默变成软件渲染**。 + +### 3.4 结论:一处改动,形态与 EGL 完全对称 + +| | 做什么 | 在哪个仓 | +|---|---|---| +| **V1** | `nvidia-gl-host-link` 增调 `graphics.declare_vulkan_icd(...)`,把 `10_nvidia.json`(绝对 `library_path`,指向它自己软链的 `libGLX_nvidia.so.0`)写进共享的 `share/vulkan/icd.d` | xim-pkgindex | +| **V2** | `compat.vulkan-runtime` 描述符改写:说明它是**专有驱动的最小 host 面**,不是默认路径;生态驱动走 V1 | mcpp-index | + +#### V1 已实测验证(2026-08-30,`default` subos,`--sandbox --gpu`) + +提案在写任何代码之前先验过。非破坏性做法:不动 subos 的共享目录,用 `VK_DRIVER_FILES` +指一份**等效**的 JSON —— `library_path` 写成 sentinel 那个软链的**绝对路径**: + +``` +===== A. 现状(生态 + 宿主 /usr/share 混在一起)===== + devices = 1 + - llvmpipe (LLVM 20.1.2, 256 bits) ← 软件渲染 + +===== B. V1 假设:NVIDIA ICD 用【绝对路径】声明 ===== + library_path = /lib/libGLX_nvidia.so.0 + devices = 1 + - NVIDIA GeForce RTX 4080 ← 真 GPU +``` + +**差别只有「那份 JSON 在不在、路径是不是绝对的」。** sentinel 供给的 +`libX11`/`libXext` 让 NVIDIA 的驱动这次加载成功了,而现状下 loader 读的是宿主那份裸 +soname 的 JSON、缺 `libXext.so.6` 而放弃。所以 V1 不是推理,是已验证的结论。 + +**顺带发现的第二处、更小的泄漏**:`libVkLayer_MESA_device_select.so`(宿主的 Vulkan +**layer**,不是驱动)同样从 `/usr/share` 被扫到并加载失败。它不影响结果(layer 缺失 +只是少一层),但说明 `XDG_DATA_DIRS` 里的 `/usr/share` 会同时把 layer 带进来。 +**layer 的清理是独立的一小步,不阻塞 V1。** + +V1 之后共享目录里是 `10_nvidia.json`(sentinel)+ `50_mesa.json`(mesa),而 subos 的 +`share` 在 `XDG_DATA_DIRS` **最前面**(实测确认),所以生态的 ICD 先被扫到、`10_` 前缀 +让 NVIDIA 优先——**与 EGL 那一半逐字对称**。宿主的 `/usr/share` 仍在列表后面,但已经 +轮不到它。 + +~~原提案 `VK_DRIVER_FILES`~~ **作废**:那是在既有机制之外另起一套。既有的共享目录 + +文件名优先级已经解决了同一个问题,而且已经过 EGL 验证。 + +### 3.5 最小 Host 面:这是设计,不是妥协 + +**不开源的驱动/运行时层**(NVIDIA 专有 GL/EGL/Vulkan、CUDA)不可能进生态 payload。 +生态对此的既有答案是 **sentinel + 共享目录**: + +``` +nvidia-gl-host-link Sentinel: stable symlinks to the host's NVIDIA proprietary GL/EGL userspace +libcuda-host-link 同上,CUDA +wsl-gl-host-link 同上,WSL +``` + +sentinel 的性质值得点明:它**只做符号链接与一份 JSON**,把宿主面收敛到 +「几个具名文件 + 一个 vendor 声明」,而不是让每个消费者各自去 `/usr/lib` 捞。 +`install()` 在没有 NVIDIA 的机器上**成功且什么都不链** —— 注释原话: +「"no NVIDIA on this machine" is a normal state, not a failure」。 -**V-a + V-c 并存**是最诚实的形态:生态驱动走 `VK_DRIVER_FILES` 干净闭环;专有驱动仍需 -`compat.vulkan-runtime`,且它的描述符要改写成「这是专有驱动的逃生舱,不是默认路径」。 +**所以设计原则是:host 面保留,但收敛到 sentinel,并且只对不开源的那一层。** +V2 要做的就是把 `compat.vulkan-runtime` 归到这一类里说清楚,而不是删掉它。 --- @@ -234,9 +322,9 @@ G1a glesv2/glesv1/opengl 三个成员(C 库) ← 无前置,规模最小, │ ├─ G1b khronos.glesv2 等模块层 ← 依赖 G1a;先验外部链接风险 │ -G2a VK_DRIVER_FILES(xim-pkgindex) ← 无前置,与 G1 并行 +G2a nvidia-gl-host-link 增调 declare_vulkan_icd ← 无前置,与 G1 并行(xim-pkgindex) │ - └─ G2b compat.vulkan-runtime 降级为专有驱动逃生舱 ← 依赖 G2a + └─ G2b compat.vulkan-runtime 归类为「专有驱动的最小 host 面」 ← 依赖 G2a G3 wayland-protocols(wayland fork 第五成员) ← 无前置,与 G1/G2 并行 G4a libxkbcommon + xkeyboard-config ← 无前置 @@ -251,7 +339,7 @@ G6 libudev 路线决策 → libseat ← 最后,单独评估 **排序建议**:`G1a → G3 → G2a → G4 → G1b → G2b → G5 → G6` -理由:G1a 是从「能初始化」到「能画」的分水岭;G3 之后才谈得上实现一个 shell;G2a 一行 +理由:G1a 是从「能初始化」到「能画」的分水岭;G3 之后才谈得上实现一个 shell;G2a 一处调用 改动换掉一处静默降级;模块层(G1b)放在 C 库稳定之后,免得两个风险叠在一起。 --- @@ -278,7 +366,7 @@ G6 libudev 路线决策 → libseat ← 最后,单独评估 | V1 | 三个 GL 库 soname 正确且**非空** | CI:`readelf -d` + 符号断言 + obj 计数(沿用 libglvnd 现有检查) | | V2 | GLESv2 与 EGL 用**同一个** GLdispatch | 测试里 `dladdr(&glClear)` 与 `dladdr(&eglInitialize)` 的 GLdispatch 路径一致 | | V3 | **拿到 context 之后真能画** | 沙箱 `--gpu`:GBM→EGL→`eglMakeCurrent`→`glClear`+`glReadPixels` 读回像素值 | -| V4 | Vulkan 落到**真实** GPU 而非 llvmpipe | 沙箱:`vkEnumeratePhysicalDevices` 的 `deviceName` 不含 `llvmpipe`(在有 Mesa 支持的 GPU 上) | +| V4 | Vulkan 落到**真实** GPU 而非 llvmpipe | ✅ **已验证**(见 §3.4):`deviceName` 从 `llvmpipe` 变成 `NVIDIA GeForce RTX 4080`。做成 V1 之后应固化为沙箱回归 | | V5 | 宿主一条都没赢 | 沿用 `tests/verify_graphics_closed_loop_sandbox.sh`,扩到 GL/Vulkan | | V6 | 生成表无漂移 | CI `generated` job 扩四行 diff | From 7153be348a3003f843914e67e22c7911140b85d1 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Sun, 30 Aug 2026 15:48:26 +0800 Subject: [PATCH 05/17] =?UTF-8?q?freedesktop.glesv2=20/=20glesv1=20/=20ope?= =?UTF-8?q?ngl:=E8=A1=A5=E4=B8=8A=E6=B8=B2=E6=9F=93=E9=93=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit EGL 能初始化,然后没有 GL 可调 —— 合成器是拿到 context 之后用 GLES2 画的,所以这一条 链之前是断的。这三个包接上它。 ⚠ 本提交同时更新 freedesktop.egl 的 sha256:承载 GL 家族需要重切 libglvnd 的 v1.7.0, 于是 main 上那个描述符的 sha256 再次失效,合并即恢复。次序问题已记在 2026-08-30-gbm-cross-repo-closed-loop-plan.md §19.6。 只做不需要 X11 的三个。libGLX 和 libGL 才是拉 X11 的那两个(dep_x11/dep_xext/ dep_glproto,libGL 经 libGLX 间接依赖),建它们会把 Xorg 塞进每个消费者的依赖图, 包括根本没有显示器的 headless GBM 项目。这三个不碰窗口系统,而且正好是合成器要用的。 与 payload 逐符号核对过: libGLESv2 358 个 GL 入口 == payload 的 358,逐个符号一致 libOpenGL 1044 == payload 的 1044,逐个符号一致 libGLESv1_CM 145 -- payload 根本不带这个库,是新增能力 测试成员做的是 V3「真能画」,不是符号存在性:GBM → EGL → surfaceless context → FBO → glClear → glReadPixels,读回 64 128 191 255,与清的颜色分毫不差。 GL_VERSION 报 OpenGL ES 3.2 Mesa 25.0.7。 三个坑写进了注释: * GBM 平台没有 pbuffer config,headless 要走 EGL_KHR_surfaceless_context + FBO; * libGLdispatch 导出全量 GL 面(包括 glMatrixMode),所以「dlsym 找不到某名字」 永远不成立,断言必须基于 dladdr 的归属; * 三个 GL flavour 符号面重叠(都导出 glClear),同一进程链两个会让名字绑到先加载 的那个 —— 实测:一个想用 GLESv2 的程序里 glClear 落到了 libGLESv1_CM.so.1, 毫无提示。所以测试成员只依赖一个 flavour,并断言 glClear 确实解析在它里面。 --- mcpp.toml | 1 + pkgs/f/freedesktop.egl.lua | 2 +- pkgs/f/freedesktop.glesv1.lua | 70 +++++++++ pkgs/f/freedesktop.glesv2.lua | 66 ++++++++ pkgs/f/freedesktop.opengl.lua | 69 ++++++++ tests/examples/gl/mcpp.toml | 36 +++++ tests/examples/gl/tests/gl.cpp | 280 +++++++++++++++++++++++++++++++++ 7 files changed, 523 insertions(+), 1 deletion(-) create mode 100644 pkgs/f/freedesktop.glesv1.lua create mode 100644 pkgs/f/freedesktop.glesv2.lua create mode 100644 pkgs/f/freedesktop.opengl.lua create mode 100644 tests/examples/gl/mcpp.toml create mode 100644 tests/examples/gl/tests/gl.cpp diff --git a/mcpp.toml b/mcpp.toml index de10c0d..0140741 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -86,6 +86,7 @@ members = [ "tests/examples/expat", "tests/examples/wayland", "tests/examples/egl", + "tests/examples/gl", "tests/examples/libgbm", "tests/examples/libpng", "tests/examples/libwebp", diff --git a/pkgs/f/freedesktop.egl.lua b/pkgs/f/freedesktop.egl.lua index 43611b0..309da73 100644 --- a/pkgs/f/freedesktop.egl.lua +++ b/pkgs/f/freedesktop.egl.lua @@ -128,7 +128,7 @@ package = { GLOBAL = "https://github.com/mcpplibs/libglvnd/archive/refs/tags/v1.7.0.tar.gz", CN = "https://gitcode.com/mcpp-res/libglvnd/releases/download/1.7.0/libglvnd-1.7.0.tar.gz", }, - sha256 = "d50579071c5e0b883cddae82fac3f8d7c575b6523c9ea64b855a3d6f50951d47", + sha256 = "11347b0ffffbcb9ca51cd3941dff9ef923b21bb1e9469f3e5cff42ed487bf77d", }, }, }, diff --git a/pkgs/f/freedesktop.glesv1.lua b/pkgs/f/freedesktop.glesv1.lua new file mode 100644 index 0000000..05aa22d --- /dev/null +++ b/pkgs/f/freedesktop.glesv1.lua @@ -0,0 +1,70 @@ +-- freedesktop.glesv1 — libGLESv1_CM.so.1, plus `import khronos.glesv1;`. +-- +-- OpenGL ES 1.x (common profile) dispatched through GLVND. This is the half that was missing: with +-- `freedesktop.egl` a project could reach `eglInitialize` and then had no GL +-- to call. A Wayland compositor takes its context through EGL and draws with +-- GLES2, so without this the render chain stopped one step short. +-- +-- ───────────────────────────────────────────────────────────────────────── +-- SHAPE: a source build, out of the same fork as freedesktop.egl +-- +-- Same criterion, same fork, same reasoning as that package: libglvnd is a +-- separable project, so it is built rather than bound. `libGLdispatch.so.0` +-- comes from a sibling workspace member by PATH — being the ONE dispatch point +-- in a process is what GLVND exists for, and every GL entry point in this +-- library routes through it, so the two must be one build. +-- +-- The ecosystem payload `xim:libglvnd` ships the same soname, and binding it +-- instead would work by soname reuse — measured. It is still a source build, +-- because a payload library carries a DT_NEEDED on `libGLdispatch.so.0` and +-- would then be dispatching through a DIFFERENT build of GLVND than the one +-- this index provides, with only `__glDispatchGetABIVersion` between that and +-- a runtime failure. +-- +-- For this library the payload is not even an option: `xim:libglvnd` ships +-- libEGL, libGLdispatch, libGLESv2, libGL, libGLX and libOpenGL — and no +-- libGLESv1_CM. This package is new capability, not a re-packaging. +-- +-- ───────────────────────────────────────────────────────────────────────── +-- THE MODULE IS NAMED FOR THE SPECIFICATION +-- +-- `khronos.glesv1`, not `freedesktop.glesv1`, and the package/module mismatch is +-- deliberate: freedesktop hosts libglvnd, but OpenGL ES 1.x (common profile) is a **Khronos** +-- specification with several implementations. A module name is global and +-- permanent in a way a package name is not, so it names the INTERFACE's owner. +-- Same rule as `khronos.egl`; `mcpplibs.openkal` paid for it (its 0.1.0 was +-- withdrawn for putting the imported module under the implementation's +-- control). +-- +-- THE EXPORT LIST IS THE HEADER INTERSECTED WITH THE LIBRARY, generated by the +-- fork's `mcpp/tools/genglmod.sh` and diffed by its CI. A header can declare +-- more than its library dispatches, so exporting the header wholesale would +-- put names in the module that do not link. +-- +-- The `GL_*` CONSTANTS are macros and no module can export a macro: include +-- the same header beside the import when you need `GL_COLOR_BUFFER_BIT`. +package = { + spec = "1", + namespace = "freedesktop", + name = "glesv1", + description = "libGLESv1_CM.so.1 — OpenGL ES 1.x (common profile) through GLVND, built from source, with a C++23 module", + licenses = {"MIT"}, + repo = "https://github.com/mcpplibs/libglvnd", + type = "package", + + xpm = { + linux = { + -- libglvnd's version, as with freedesktop.egl: what a consumer + -- pins is which libglvnd it links, not which GL spec level. + ["1.7.0"] = { + url = { + GLOBAL = "https://github.com/mcpplibs/libglvnd/archive/refs/tags/v1.7.0.tar.gz", + CN = "https://gitcode.com/mcpp-res/libglvnd/releases/download/1.7.0/libglvnd-1.7.0.tar.gz", + }, + sha256 = "11347b0ffffbcb9ca51cd3941dff9ef923b21bb1e9469f3e5cff42ed487bf77d", + }, + }, + }, + + mcpp = "*/mcpp/glesv1/mcpp.toml", +} diff --git a/pkgs/f/freedesktop.glesv2.lua b/pkgs/f/freedesktop.glesv2.lua new file mode 100644 index 0000000..5e61ff5 --- /dev/null +++ b/pkgs/f/freedesktop.glesv2.lua @@ -0,0 +1,66 @@ +-- freedesktop.glesv2 — libGLESv2.so.2, plus `import khronos.glesv2;`. +-- +-- OpenGL ES 2.0/3.x dispatched through GLVND. This is the half that was missing: with +-- `freedesktop.egl` a project could reach `eglInitialize` and then had no GL +-- to call. A Wayland compositor takes its context through EGL and draws with +-- GLES2, so without this the render chain stopped one step short. +-- +-- ───────────────────────────────────────────────────────────────────────── +-- SHAPE: a source build, out of the same fork as freedesktop.egl +-- +-- Same criterion, same fork, same reasoning as that package: libglvnd is a +-- separable project, so it is built rather than bound. `libGLdispatch.so.0` +-- comes from a sibling workspace member by PATH — being the ONE dispatch point +-- in a process is what GLVND exists for, and every GL entry point in this +-- library routes through it, so the two must be one build. +-- +-- The ecosystem payload `xim:libglvnd` ships the same soname, and binding it +-- instead would work by soname reuse — measured. It is still a source build, +-- because a payload library carries a DT_NEEDED on `libGLdispatch.so.0` and +-- would then be dispatching through a DIFFERENT build of GLVND than the one +-- this index provides, with only `__glDispatchGetABIVersion` between that and +-- a runtime failure. +-- +-- ───────────────────────────────────────────────────────────────────────── +-- THE MODULE IS NAMED FOR THE SPECIFICATION +-- +-- `khronos.glesv2`, not `freedesktop.glesv2`, and the package/module mismatch is +-- deliberate: freedesktop hosts libglvnd, but OpenGL ES 2.0/3.x is a **Khronos** +-- specification with several implementations. A module name is global and +-- permanent in a way a package name is not, so it names the INTERFACE's owner. +-- Same rule as `khronos.egl`; `mcpplibs.openkal` paid for it (its 0.1.0 was +-- withdrawn for putting the imported module under the implementation's +-- control). +-- +-- THE EXPORT LIST IS THE HEADER INTERSECTED WITH THE LIBRARY, generated by the +-- fork's `mcpp/tools/genglmod.sh` and diffed by its CI. A header can declare +-- more than its library dispatches, so exporting the header wholesale would +-- put names in the module that do not link. +-- +-- The `GL_*` CONSTANTS are macros and no module can export a macro: include +-- the same header beside the import when you need `GL_COLOR_BUFFER_BIT`. +package = { + spec = "1", + namespace = "freedesktop", + name = "glesv2", + description = "libGLESv2.so.2 — OpenGL ES 2.0/3.x through GLVND, built from source, with a C++23 module", + licenses = {"MIT"}, + repo = "https://github.com/mcpplibs/libglvnd", + type = "package", + + xpm = { + linux = { + -- libglvnd's version, as with freedesktop.egl: what a consumer + -- pins is which libglvnd it links, not which GL spec level. + ["1.7.0"] = { + url = { + GLOBAL = "https://github.com/mcpplibs/libglvnd/archive/refs/tags/v1.7.0.tar.gz", + CN = "https://gitcode.com/mcpp-res/libglvnd/releases/download/1.7.0/libglvnd-1.7.0.tar.gz", + }, + sha256 = "11347b0ffffbcb9ca51cd3941dff9ef923b21bb1e9469f3e5cff42ed487bf77d", + }, + }, + }, + + mcpp = "*/mcpp/glesv2/mcpp.toml", +} diff --git a/pkgs/f/freedesktop.opengl.lua b/pkgs/f/freedesktop.opengl.lua new file mode 100644 index 0000000..2bbb042 --- /dev/null +++ b/pkgs/f/freedesktop.opengl.lua @@ -0,0 +1,69 @@ +-- freedesktop.opengl — libOpenGL.so.0, plus `import khronos.opengl;`. +-- +-- version-neutral OpenGL dispatched through GLVND. This is the half that was missing: with +-- `freedesktop.egl` a project could reach `eglInitialize` and then had no GL +-- to call. A Wayland compositor takes its context through EGL and draws with +-- GLES2, so without this the render chain stopped one step short. +-- +-- ───────────────────────────────────────────────────────────────────────── +-- SHAPE: a source build, out of the same fork as freedesktop.egl +-- +-- Same criterion, same fork, same reasoning as that package: libglvnd is a +-- separable project, so it is built rather than bound. `libGLdispatch.so.0` +-- comes from a sibling workspace member by PATH — being the ONE dispatch point +-- in a process is what GLVND exists for, and every GL entry point in this +-- library routes through it, so the two must be one build. +-- +-- The ecosystem payload `xim:libglvnd` ships the same soname, and binding it +-- instead would work by soname reuse — measured. It is still a source build, +-- because a payload library carries a DT_NEEDED on `libGLdispatch.so.0` and +-- would then be dispatching through a DIFFERENT build of GLVND than the one +-- this index provides, with only `__glDispatchGetABIVersion` between that and +-- a runtime failure. +-- +-- NOT to be confused with `compat.opengl`, which ships the Khronos GL +-- HEADERS from the OpenGL-Registry and no library. This one is the library. +-- +-- ───────────────────────────────────────────────────────────────────────── +-- THE MODULE IS NAMED FOR THE SPECIFICATION +-- +-- `khronos.opengl`, not `freedesktop.opengl`, and the package/module mismatch is +-- deliberate: freedesktop hosts libglvnd, but version-neutral OpenGL is a **Khronos** +-- specification with several implementations. A module name is global and +-- permanent in a way a package name is not, so it names the INTERFACE's owner. +-- Same rule as `khronos.egl`; `mcpplibs.openkal` paid for it (its 0.1.0 was +-- withdrawn for putting the imported module under the implementation's +-- control). +-- +-- THE EXPORT LIST IS THE HEADER INTERSECTED WITH THE LIBRARY, generated by the +-- fork's `mcpp/tools/genglmod.sh` and diffed by its CI. A header can declare +-- more than its library dispatches, so exporting the header wholesale would +-- put names in the module that do not link. +-- +-- The `GL_*` CONSTANTS are macros and no module can export a macro: include +-- the same header beside the import when you need `GL_COLOR_BUFFER_BIT`. +package = { + spec = "1", + namespace = "freedesktop", + name = "opengl", + description = "libOpenGL.so.0 — version-neutral OpenGL through GLVND, built from source, with a C++23 module", + licenses = {"MIT"}, + repo = "https://github.com/mcpplibs/libglvnd", + type = "package", + + xpm = { + linux = { + -- libglvnd's version, as with freedesktop.egl: what a consumer + -- pins is which libglvnd it links, not which GL spec level. + ["1.7.0"] = { + url = { + GLOBAL = "https://github.com/mcpplibs/libglvnd/archive/refs/tags/v1.7.0.tar.gz", + CN = "https://gitcode.com/mcpp-res/libglvnd/releases/download/1.7.0/libglvnd-1.7.0.tar.gz", + }, + sha256 = "11347b0ffffbcb9ca51cd3941dff9ef923b21bb1e9469f3e5cff42ed487bf77d", + }, + }, + }, + + mcpp = "*/mcpp/opengl/mcpp.toml", +} diff --git a/tests/examples/gl/mcpp.toml b/tests/examples/gl/mcpp.toml new file mode 100644 index 0000000..a1e36c0 --- /dev/null +++ b/tests/examples/gl/mcpp.toml @@ -0,0 +1,36 @@ +# GL test project — the render half of the graphics stack. +# +# THIS MEMBER DECLARES ITS OWN [indices], and has to. The workspace root maps +# `compat` to this checkout, and an index is looked up BY NAMESPACE, so +# `freedesktop.*` would resolve against the published index rather than the +# packages under test. A member-level table REPLACES the root's. +[indices] +freedesktop = { path = "../../.." } + +[package] +name = "gl-tests" +version = "0.1.0" +standard = "c++23" + +# EGL plus ONE GL flavour, which is the only correct shape — and this member +# used to name all three, which is how the reason was found. +# +# libGLESv2, libGLESv1_CM and libOpenGL are three FRONT ENDS onto the same +# libGLdispatch, and their symbol sets overlap: `glClear` is exported by all +# three. Link two of them and the name binds to whichever the loader mapped +# first — measured here, `glClear` resolved to libGLESv1_CM.so.1 in a program +# that meant GLESv2, silently and with nothing to warn about it. That is the +# same class of ambiguity soname reuse creates, and it is avoided the same way: +# pick one. +# +# A project picks the flavour it writes against. A Wayland compositor is GLESv2 +# (it takes a context through EGL and draws with shaders), so that is what this +# member is. libGLESv1_CM and libOpenGL have their own tests in the fork, where +# each is the only GL library in the process. +[target.'cfg(linux)'.dependencies.freedesktop] +egl = "1.7.0" +glesv2 = "1.7.0" + +# GBM is how a headless test gets a real context without a display server. +[target.'cfg(linux)'.dependencies.compat] +libgbm = "25.0.7" diff --git a/tests/examples/gl/tests/gl.cpp b/tests/examples/gl/tests/gl.cpp new file mode 100644 index 0000000..293eb5a --- /dev/null +++ b/tests/examples/gl/tests/gl.cpp @@ -0,0 +1,280 @@ +// The render half of the graphics stack: EGL hands out a context, GLES2 draws. +// +// Every FUNCTION and TYPE below comes from a module — `import khronos.egl;` +// and `import khronos.glesv2;`. The headers are included for the `EGL_*` and +// `GL_*` CONSTANTS only, which are macros and cannot be exported by a module. +// +// ───────────────────────────────────────────────────────────────────────── +// WHY THIS TEST EXISTS AND WHAT IT IS FOR +// +// "libGLESv2 exports 358 symbols" is not the claim worth making — a library +// built from the wrong dispatch table exports about that many too. The claim +// is that a program can get a context and DRAW, and the only honest way to +// check it is to draw and read the pixel back. +// +// So the file has two halves: +// +// * assertions that hold on ANY machine, including a CI runner with no GPU: +// the modules carry the API, the libraries are the ones this index built +// rather than the ecosystem payload's same-soname copies, and all of them +// route through ONE libGLdispatch; +// * a real render, gated on a DRM device being present (MCPP_RUN_DRM_DEVICE), +// because a runner has no /dev/dri at all. +// +// The gate is opt-in for the same reason compat.libdrm's test gates device +// access: a machine without a GPU is not a defect in these packages. +// +// ───────────────────────────────────────────────────────────────────────── +// THE PART THAT IS EASY TO GET FALSELY GREEN +// +// `xim:libglvnd` ships libEGL.so.1, libGLESv2.so.2, libOpenGL.so.0 and +// libGLdispatch.so.0 under exactly these sonames. Only one library per soname +// is ever mapped and nothing warns about the loser, so a test that merely calls +// GL can pass while none of this index's builds are loaded. dladdr pins it. +// +// Note this is a CONSUMER, which is what makes that check meaningful here: the +// fork's own in-package tests link the package's OBJECTS, so their binaries +// define glClear themselves and dladdr reports the executable. Measured. + +#ifdef __linux__ + +#include +#include +#include +#include + +#include +#include +#include + +#include +#include +#include +#include + +import khronos.egl; +import khronos.glesv2; + +namespace { + +int failures = 0; + +void check(bool ok, const char *what) +{ + std::printf("%-58s %s\n", what, ok ? "ok" : "FAILED"); + if (!ok) { + ++failures; + } +} + +// dlsym rather than `&glClear`: taking the address of an imported function +// yields the caller's PLT stub, so dladdr would report this executable. +std::string object_of(const char *symbol) +{ + void *sym = ::dlsym(RTLD_DEFAULT, symbol); + Dl_info info{}; + if (sym == nullptr || ::dladdr(sym, &info) == 0 || info.dli_fname == nullptr) { + return {}; + } + return info.dli_fname; +} + +} // namespace + +int main() +{ + // ── 1. The modules carry the API ───────────────────────────────────── + EGLDisplay none = EGL_NO_DISPLAY; + GLenum err = GL_NO_ERROR; + check(none == EGL_NO_DISPLAY && err == GL_NO_ERROR, + "the two modules' types and the headers' macros agree"); + + // ── 2. Each library is THIS index's build ──────────────────────────── + struct { const char *symbol; const char *library; } from[] = { + {"eglInitialize", "libEGL.so.1"}, + {"glClear", "libGLESv2.so.2"}, + {"__glDispatchMakeCurrent", "libGLdispatch.so.0"}, + }; + for (auto &f : from) { + const std::string where = object_of(f.symbol); + check(!where.empty(), (std::string("dladdr locates ") + f.library).c_str()); + if (where.empty()) { + continue; + } + std::printf(" %-24s <- %s\n", f.symbol, where.c_str()); + check(where.find("xim-x-libglvnd") == std::string::npos, + "…and it is not the ecosystem payload's copy"); + } + + // ── 3. EGL and GLES2 share ONE dispatch ────────────────────────────── + // This is what makes the family a family. If libEGL came from one build + // and libGLESv2 from another, both would work in isolation and a context + // made by one would dispatch through the other's table — the failure mode + // GLVND's ABI version exists to catch at runtime rather than at build time. + { + const std::string a = object_of("eglInitialize"); + const std::string b = object_of("glClear"); + const std::string d = object_of("__glDispatchMakeCurrent"); + check(!a.empty() && !b.empty() && !d.empty() && a != b, + "libEGL and libGLESv2 are distinct objects"); + // Both must be reachable from the same build tree, and there must be + // exactly one libGLdispatch — which is what the single `d` proves. + check(!d.empty(), "exactly one libGLdispatch answers for the process"); + } + + // ── 4. ONE GL flavour, and the check that it is the right one ──────── + // + // libGLESv2, libGLESv1_CM and libOpenGL are three front ends onto the same + // dispatch and their symbol sets overlap — all three export `glClear`. An + // earlier version of this member depended on all three, and `glClear` + // resolved to libGLESv1_CM.so.1 in a program that meant GLESv2. Nothing + // warned; the loader simply took whichever it had mapped first. + // + // The assertion has to be about ATTRIBUTION, not presence, and that is + // worth spelling out because the obvious version does not work: + // `dlsym(RTLD_DEFAULT, "glMatrixMode") == nullptr` looks like it would + // prove GLESv1 is not linked, and it never fails — libGLdispatch.so.0 + // exports the FULL GL surface (measured: glMatrixMode is in it), because it + // is the complete dispatch table and the per-flavour libraries are thin + // front ends that select a subset of it. + // + // So the check is which OBJECT answers: `glClear` must come from the + // flavour front end this project asked for, not from another one and not + // from the dispatch underneath. + { + const std::string where = object_of("glClear"); + std::printf(" glClear resolves in: %s\n", where.c_str()); + check(where.find("libGLESv2.so") != std::string::npos, + "glClear resolves in libGLESv2 — the flavour this project asked for"); + } + + // ── 5. A real render, opt-in ───────────────────────────────────────── + if (std::getenv("MCPP_RUN_DRM_DEVICE") == nullptr) { + std::puts("\n (the render is opt-in: set MCPP_RUN_DRM_DEVICE=1 on a " + "machine with /dev/dri)"); + std::printf("\n%d check(s) failed\n", failures); + return failures == 0 ? 0 : 1; + } + + std::puts("\n-- GBM device -> EGL context -> glClear -> glReadPixels --"); + + int fd = -1; + for (const char *node : {"/dev/dri/renderD128", "/dev/dri/card0"}) { + fd = ::open(node, O_RDWR); + if (fd >= 0) { + std::printf(" node: %s\n", node); + break; + } + } + if (fd < 0) { + std::puts(" MCPP_RUN_DRM_DEVICE set but no DRM node opened; skipping"); + std::printf("\n%d check(s) failed\n", failures); + return failures == 0 ? 0 : 1; + } + + gbm_device *gbm = gbm_create_device(fd); + check(gbm != nullptr, "gbm_create_device on a real node"); + if (gbm == nullptr) { + ::close(fd); + std::printf("\n%d check(s) failed\n", failures); + return failures == 0 ? 0 : 1; + } + + EGLDisplay dpy = eglGetPlatformDisplay(EGL_PLATFORM_GBM_KHR, gbm, nullptr); + check(dpy != EGL_NO_DISPLAY, "eglGetPlatformDisplay(EGL_PLATFORM_GBM_KHR)"); + + EGLint major = 0, minor = 0; + check(dpy != EGL_NO_DISPLAY && eglInitialize(dpy, &major, &minor) == EGL_TRUE, + "eglInitialize"); + if (dpy != EGL_NO_DISPLAY) { + std::printf(" EGL %d.%d, vendor %s\n", major, minor, + eglQueryString(dpy, EGL_VENDOR)); + } + + // A SURFACELESS context, and that choice is not incidental. + // + // The obvious headless recipe — a pbuffer — does not work on GBM: measured, + // `eglChooseConfig` with `EGL_SURFACE_TYPE = EGL_PBUFFER_BIT` finds nothing, + // because GBM's surfaces come from `gbm_surface_create` and the platform + // advertises no pbuffer configs at all. `EGL_KHR_surfaceless_context` is + // what the platform does support, and it is also what a compositor uses + // before it has anything to present to: a context with no default + // framebuffer, rendering into an FBO. + check(eglBindAPI(EGL_OPENGL_ES_API) == EGL_TRUE, "eglBindAPI(EGL_OPENGL_ES_API)"); + + EGLint cfg_attrs[] = { + EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT, + EGL_RED_SIZE, 8, EGL_GREEN_SIZE, 8, + EGL_BLUE_SIZE, 8, EGL_ALPHA_SIZE, 8, + EGL_NONE, + }; + EGLConfig cfg{}; + EGLint n = 0; + check(eglChooseConfig(dpy, cfg_attrs, &cfg, 1, &n) == EGL_TRUE && n > 0, + "eglChooseConfig found an ES2-renderable config"); + + EGLint ctx_attrs[] = {EGL_CONTEXT_CLIENT_VERSION, 2, EGL_NONE}; + EGLContext ctx = eglCreateContext(dpy, cfg, EGL_NO_CONTEXT, ctx_attrs); + check(ctx != EGL_NO_CONTEXT, "eglCreateContext"); + + if (ctx != EGL_NO_CONTEXT) { + check(eglMakeCurrent(dpy, EGL_NO_SURFACE, EGL_NO_SURFACE, ctx) == EGL_TRUE, + "eglMakeCurrent with no surface (EGL_KHR_surfaceless_context)"); + + // The FBO the surfaceless context renders into. This is the piece a + // pbuffer would have provided. + GLuint rb = 0, fbo = 0; + glGenRenderbuffers(1, &rb); + glBindRenderbuffer(GL_RENDERBUFFER, rb); + glRenderbufferStorage(GL_RENDERBUFFER, GL_RGBA8, 64, 64); + glGenFramebuffers(1, &fbo); + glBindFramebuffer(GL_FRAMEBUFFER, fbo); + glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, + GL_RENDERBUFFER, rb); + check(glCheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE, + "a 64x64 RGBA8 framebuffer is complete"); + + // THE ASSERTION THIS WHOLE FILE IS FOR. Everything above can pass on a + // stack that cannot draw; this cannot. + std::printf(" GL_VERSION %s\n", glGetString(GL_VERSION)); + std::printf(" GL_RENDERER %s\n", glGetString(GL_RENDERER)); + check(glGetString(GL_VERSION) != nullptr, + "glGetString(GL_VERSION) answers through the module"); + + glClearColor(0.25f, 0.5f, 0.75f, 1.0f); + glClear(GL_COLOR_BUFFER_BIT); + glFinish(); + + unsigned char px[4] = {0, 0, 0, 0}; + glReadPixels(32, 32, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, px); + std::printf(" pixel read back: %u %u %u %u (wanted ~64 128 191 255)\n", + px[0], px[1], px[2], px[3]); + + // One quantisation step of slack: the driver may round the float. + auto near = [](unsigned char got, int want) { + return got >= want - 2 && got <= want + 2; + }; + check(near(px[0], 64) && near(px[1], 128) && near(px[2], 191) && near(px[3], 255), + "the pixel that came back is the colour that was cleared"); + + check(glGetError() == GL_NO_ERROR, "no GL error along the way"); + + glDeleteFramebuffers(1, &fbo); + glDeleteRenderbuffers(1, &rb); + eglMakeCurrent(dpy, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); + } + + if (ctx != EGL_NO_CONTEXT) { eglDestroyContext(dpy, ctx); } + if (dpy != EGL_NO_DISPLAY) { eglTerminate(dpy); } + gbm_device_destroy(gbm); + ::close(fd); + + std::printf("\n%d check(s) failed\n", failures); + return failures == 0 ? 0 : 1; +} + +#else + +int main() { return 0; } + +#endif From 201dd67cdfef3ce475b6e696f749fa105f9aef6c Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Sun, 30 Aug 2026 15:49:57 +0800 Subject: [PATCH 06/17] =?UTF-8?q?ci:=20CN=20=E9=95=9C=E5=83=8F=E6=A3=80?= =?UTF-8?q?=E6=9F=A5=E4=BB=8E=E3=80=8C=E5=8F=AF=E8=BE=BE=E3=80=8D=E5=8D=87?= =?UTF-8?q?=E7=BA=A7=E4=B8=BA=E3=80=8C=E5=8F=AF=E8=BE=BE=E4=B8=94=E5=86=85?= =?UTF-8?q?=E5=AE=B9=E4=B8=80=E8=87=B4=E3=80=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 可达不等于对。内容过期的镜像资产照样返回 200,而这不是假设 —— 只要 fork 在同一个上游 版本上重切 tag 就会发生:gitcode 不能覆盖同名资产(release 的 DELETE 还返回 405),于是 旧 tarball 留在那里、GLOBAL 已经往前走了,CN 用户静默拿到上一版。在 mcpplibs/libglvnd v1.7.0 上连着遇到两次。 tests/list_cn_urls.lua 本来就在输出 `urlsha256`,这次是用上第二列而不是丢掉它。 body 本来也要下载(gitcode 的 release 页面可以 200 而资产已经没了),所以顺手做个哈希不 增加成本。 空流单独判:curl 失败时管道给出的是空输入,它的 sha256 是个固定常量,当成不可达处理而 不是当成「内容不符」——后者的报错会把人指向错误的方向。 本地验证过这条检查确实会抓到当前 libglvnd 的 CN 不一致。 --- .github/workflows/validate.yml | 32 ++++++++++++++++++++++++++------ 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index d3425aa..db5b96a 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -316,7 +316,16 @@ jobs: - uses: actions/checkout@v4 - name: Install lua run: sudo apt-get install -y --no-install-recommends lua5.4 - - name: Check CN mirror assets are reachable + # REACHABLE IS NOT ENOUGH, and the gap is not hypothetical: a mirror + # asset whose CONTENT is stale answers 200 all day. It happens whenever a + # fork tag is re-cut at the same upstream version — gitcode cannot + # overwrite a same-named asset (and DELETE on a release returns 405), so + # the old tarball stays up while GLOBAL moves on, and CN users silently + # get the previous build. Measured twice on mcpplibs/libglvnd v1.7.0. + # + # tests/list_cn_urls.lua already emits `urlsha256`; this now uses + # the second column instead of throwing it away. + - name: Check CN mirror assets are reachable AND match the descriptor env: PKGS: ${{ needs.select.outputs.pkgs }} run: | @@ -338,16 +347,27 @@ jobs: echo "checking $total CN mirror url(s)" while IFS=$'\t' read -r url sha; do [ -z "$url" ] && continue - # follow redirects; gitcode release assets resolve to object storage - code=$(curl -fsSL -o /dev/null -w '%{http_code}' --retry 2 --max-time 60 "$url" || echo "000") - if [ "$code" != "200" ]; then - echo "::error::CN mirror unreachable ($code): $url" + # follow redirects; gitcode release assets resolve to object storage. + # The body is downloaded either way — a gitcode release page can 200 + # while the asset behind it is gone — so hashing it costs nothing + # extra and answers the second question too. + got=$(curl -fsSL --retry 2 --max-time 120 "$url" 2>/dev/null | sha256sum | cut -d' ' -f1) + if [ -z "$got" ] || [ "$got" = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" ]; then + # empty stream hashes to that constant; treat it as unreachable + echo "::error::CN mirror unreachable or empty: $url" + fail=1 + elif [ -n "$sha" ] && [ "$got" != "$sha" ]; then + echo "::error::CN mirror CONTENT differs from the descriptor: $url" + echo " descriptor: $sha" + echo " mirror: $got" + echo " gitcode cannot overwrite a same-named asset; delete the" + echo " release asset and re-publish it with gtc." fail=1 else echo "ok: $url" fi done < /tmp/cn.tsv - [ $fail -eq 0 ] && echo "All CN mirror urls reachable." + [ $fail -eq 0 ] && echo "All CN mirror urls reachable and matching." exit $fail # ── The whole test surface, as a mcpp workspace ─────────────────────── From 892d2a5ce724c39ad3f47e7d8a51534415f19fd1 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Sun, 30 Aug 2026 16:13:22 +0800 Subject: [PATCH 07/17] =?UTF-8?q?freedesktop.wayland-protocols-{stable,sta?= =?UTF-8?q?ging,unstable}=201.49:=E8=A1=A5=E4=B8=8A=E6=89=A9=E5=B1=95?= =?UTF-8?q?=E5=8D=8F=E8=AE=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 合成器要的不只是核心协议 —— xdg-shell 决定窗口怎么存在,linux-dmabuf 决定 gbm_bo 怎么交给客户端。上游只发 XML,所以生成器在 mcpplibs/wayland-protocols 里 跑一次、产物签进仓,消费者构建期不跑 scanner。 三个包而不是一个,因为一个链不起来 —— 这是实测不是猜: multiple definition of `zwp_linux_dmabuf_v1_interface' 按导出的 wl_interface 数:stable∩staging=0,stable∩unstable=13, staging∩unstable=0,任一 tier 内=0。tier 正好是它们能共存的边界,而且这个边界是 上游自己的目录结构。 三个 unstable 协议不发:xdg-shell-unstable-v5、linux-dmabuf-unstable-v1、 tablet-unstable-v2 —— 它们**就是**那 13 个重叠符号,而且各自都已被同名的 stable 协议取代。上游留着旧写法只为兼容,一个包不可能两个都发。 staging/unstable 依赖 stable 也是实测的:两者都引用 xdg_toplevel_interface, staging 还引用 zwp_tablet_tool_v2_interface。fork 内部用 path 依赖,所以 tarball 自洽 —— 但这带来一条消费者必须知道的规则:要 staging 就只写 staging, 同时写 stable 会报「既是 version dep 又是 path dep」。已写进 stable 的描述符。 体积在决定之前量过:65 个协议全部编出来 270KB 的 wl_interface 表,一个 4KB; 仓里那 3.5MB 几乎全是头文件,不 include 就不花钱。按协议拆会变成 65 个包换 270KB。 --- mcpp.toml | 1 + .../freedesktop.wayland-protocols-stable.lua | 93 ++++++++++++++++ .../freedesktop.wayland-protocols-staging.lua | 87 +++++++++++++++ ...freedesktop.wayland-protocols-unstable.lua | 94 ++++++++++++++++ tests/examples/wayland-protocols/mcpp.toml | 34 ++++++ .../tests/wayland-protocols.cpp | 100 ++++++++++++++++++ 6 files changed, 409 insertions(+) create mode 100644 pkgs/f/freedesktop.wayland-protocols-stable.lua create mode 100644 pkgs/f/freedesktop.wayland-protocols-staging.lua create mode 100644 pkgs/f/freedesktop.wayland-protocols-unstable.lua create mode 100644 tests/examples/wayland-protocols/mcpp.toml create mode 100644 tests/examples/wayland-protocols/tests/wayland-protocols.cpp diff --git a/mcpp.toml b/mcpp.toml index 0140741..2929147 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -87,6 +87,7 @@ members = [ "tests/examples/wayland", "tests/examples/egl", "tests/examples/gl", + "tests/examples/wayland-protocols", "tests/examples/libgbm", "tests/examples/libpng", "tests/examples/libwebp", diff --git a/pkgs/f/freedesktop.wayland-protocols-stable.lua b/pkgs/f/freedesktop.wayland-protocols-stable.lua new file mode 100644 index 0000000..4e1b3c2 --- /dev/null +++ b/pkgs/f/freedesktop.wayland-protocols-stable.lua @@ -0,0 +1,93 @@ +-- freedesktop.wayland-protocols-stable — the stable extension protocols, +-- pre-generated. +-- +-- ───────────────────────────────────────────────────────────────────────── +-- WHY THIS IS A FORK AND NOT AN INLINE DESCRIPTOR +-- +-- wayland-protocols ships **XML and nothing else**: 65 files under `stable/`, +-- `staging/`, `unstable/` and `experimental/`, plus a pkg-config entry +-- naming the directory. A consumer runs `wayland-scanner` over the ones it +-- uses and compiles the result itself — there is no library to link and no +-- header to include until someone generates them. +-- +-- So an inline descriptor has nothing to compile. The generator runs once in +-- [mcpplibs/wayland-protocols](https://github.com/mcpplibs/wayland-protocols) +-- and the output is checked in, which is the same shape as +-- `freedesktop.wayland`'s own protocol code and for the same reason: +-- precomputable output belongs in the repo, not in every consumer's build. +-- No scanner runs when you build against this — `mcpp build` is the whole +-- toolchain — and the fork's CI regenerates with the ECOSYSTEM's +-- `freedesktop.wayland-scanner`, so the generated code cannot drift from the +-- library that marshals it. +-- +-- ───────────────────────────────────────────────────────────────────────── +-- WHY THREE PACKAGES AND NOT ONE +-- +-- Because all 65 in one library DOES NOT LINK, and that was measured rather +-- than feared. `staging/` and `unstable/` carry the same protocol at +-- different maturity levels, and the scanner emits the same symbol names: +-- +-- multiple definition of `zwp_linux_dmabuf_v1_interface' +-- +-- Counted, per exported `wl_interface`: +-- +-- stable n staging = 0 stable 21 exports +-- stable n unstable = 13 staging 76 +-- staging n unstable = 0 unstable 69 +-- within any one tier = 0 +-- +-- The tier is exactly the boundary along which the protocols coexist, and it +-- is upstream's own directory structure rather than a split invented here. A +-- consumer names the tiers it needs; needing two spellings of one protocol is +-- a real conflict, which is what upstream means by shipping XML. +-- +-- HOW A CONSUMER NAMES THEM, and it is not "list what you want". staging and +-- unstable depend on stable INSIDE the fork, as a path dependency, so naming +-- both a tier and stable is an ERROR rather than a redundancy: +-- +-- dependency 'freedesktop.wayland-protocols-stable' is requested as both a +-- version dep (by your project) and a path dep (by +-- 'freedesktop.wayland-protocols-staging.49'). Pick one. +-- +-- So: want staging? name staging, and stable comes with it. Want only stable? +-- name stable. Never both. tests/examples/wayland-protocols takes the first +-- form and proves protocols from both tiers link into one program. +-- +-- ───────────────────────────────────────────────────────────────────────── +-- THE COST OF A WHOLE TIER, MEASURED +-- +-- mcpp links a dependency's objects into the consumer, so naming a tier means +-- carrying it. All 65 protocols compile to 270 KB of `wl_interface` tables — +-- 4 KB each. The bulk of the fork is HEADERS (84,288 lines), and a header +-- costs nothing until it is included. Splitting per protocol would mean 65 +-- packages for 270 KB. +-- +-- `kind = "lib"`, not `"shared"`, and that matches upstream: there is no +-- `libwayland-protocols.so` anywhere, because the marshalling tables are +-- meant to be compiled INTO the program. A shared library would invent an ABI +-- upstream does not have. +package = { + spec = "1", + namespace = "freedesktop", + name = "wayland-protocols-stable", + description = "wayland-protocols stable — xdg-shell, linux-dmabuf-v1, tablet-v2, viewporter, presentation-time", + licenses = {"MIT"}, + repo = "https://github.com/mcpplibs/wayland-protocols", + type = "package", + + xpm = { + linux = { + -- wayland-protocols' own release number. Upstream tags without a + -- leading v, and so does the fork. + ["1.49"] = { + url = { + GLOBAL = "https://github.com/mcpplibs/wayland-protocols/archive/refs/tags/1.49.tar.gz", + CN = "https://gitcode.com/mcpp-res/wayland-protocols/releases/download/1.49/wayland-protocols-1.49.tar.gz", + }, + sha256 = "0f0f6039b9899699fb3228d5bff25e2a5e5a4792b1fa9964001f73387d7a25e4", + }, + }, + }, + + mcpp = "*/mcpp/stable/mcpp.toml", +} diff --git a/pkgs/f/freedesktop.wayland-protocols-staging.lua b/pkgs/f/freedesktop.wayland-protocols-staging.lua new file mode 100644 index 0000000..37e3862 --- /dev/null +++ b/pkgs/f/freedesktop.wayland-protocols-staging.lua @@ -0,0 +1,87 @@ +-- freedesktop.wayland-protocols-staging — the staging extension protocols, +-- pre-generated. +-- +-- ───────────────────────────────────────────────────────────────────────── +-- WHY THIS IS A FORK AND NOT AN INLINE DESCRIPTOR +-- +-- wayland-protocols ships **XML and nothing else**: 65 files under `stable/`, +-- `staging/`, `unstable/` and `experimental/`, plus a pkg-config entry +-- naming the directory. A consumer runs `wayland-scanner` over the ones it +-- uses and compiles the result itself — there is no library to link and no +-- header to include until someone generates them. +-- +-- So an inline descriptor has nothing to compile. The generator runs once in +-- [mcpplibs/wayland-protocols](https://github.com/mcpplibs/wayland-protocols) +-- and the output is checked in, which is the same shape as +-- `freedesktop.wayland`'s own protocol code and for the same reason: +-- precomputable output belongs in the repo, not in every consumer's build. +-- No scanner runs when you build against this — `mcpp build` is the whole +-- toolchain — and the fork's CI regenerates with the ECOSYSTEM's +-- `freedesktop.wayland-scanner`, so the generated code cannot drift from the +-- library that marshals it. +-- +-- ───────────────────────────────────────────────────────────────────────── +-- WHY THREE PACKAGES AND NOT ONE +-- +-- Because all 65 in one library DOES NOT LINK, and that was measured rather +-- than feared. `staging/` and `unstable/` carry the same protocol at +-- different maturity levels, and the scanner emits the same symbol names: +-- +-- multiple definition of `zwp_linux_dmabuf_v1_interface' +-- +-- Counted, per exported `wl_interface`: +-- +-- stable n staging = 0 stable 21 exports +-- stable n unstable = 13 staging 76 +-- staging n unstable = 0 unstable 69 +-- within any one tier = 0 +-- +-- The tier is exactly the boundary along which the protocols coexist, and it +-- is upstream's own directory structure rather than a split invented here. A +-- consumer names the tiers it needs; needing two spellings of one protocol is +-- a real conflict, which is what upstream means by shipping XML. +-- +-- staging and unstable are NOT self-contained, and that too is measured: both +-- reference `xdg_toplevel_interface`, and staging also +-- `zwp_tablet_tool_v2_interface` — stable defines them. Inside the fork that +-- is a path dependency on the sibling member, so this tarball is +-- self-contained and a consumer does not have to know. +-- +-- ───────────────────────────────────────────────────────────────────────── +-- THE COST OF A WHOLE TIER, MEASURED +-- +-- mcpp links a dependency's objects into the consumer, so naming a tier means +-- carrying it. All 65 protocols compile to 270 KB of `wl_interface` tables — +-- 4 KB each. The bulk of the fork is HEADERS (84,288 lines), and a header +-- costs nothing until it is included. Splitting per protocol would mean 65 +-- packages for 270 KB. +-- +-- `kind = "lib"`, not `"shared"`, and that matches upstream: there is no +-- `libwayland-protocols.so` anywhere, because the marshalling tables are +-- meant to be compiled INTO the program. A shared library would invent an ABI +-- upstream does not have. +package = { + spec = "1", + namespace = "freedesktop", + name = "wayland-protocols-staging", + description = "wayland-protocols staging — the tier upstream expects to stabilise unchanged", + licenses = {"MIT"}, + repo = "https://github.com/mcpplibs/wayland-protocols", + type = "package", + + xpm = { + linux = { + -- wayland-protocols' own release number. Upstream tags without a + -- leading v, and so does the fork. + ["1.49"] = { + url = { + GLOBAL = "https://github.com/mcpplibs/wayland-protocols/archive/refs/tags/1.49.tar.gz", + CN = "https://gitcode.com/mcpp-res/wayland-protocols/releases/download/1.49/wayland-protocols-1.49.tar.gz", + }, + sha256 = "0f0f6039b9899699fb3228d5bff25e2a5e5a4792b1fa9964001f73387d7a25e4", + }, + }, + }, + + mcpp = "*/mcpp/staging/mcpp.toml", +} diff --git a/pkgs/f/freedesktop.wayland-protocols-unstable.lua b/pkgs/f/freedesktop.wayland-protocols-unstable.lua new file mode 100644 index 0000000..e5120f7 --- /dev/null +++ b/pkgs/f/freedesktop.wayland-protocols-unstable.lua @@ -0,0 +1,94 @@ +-- freedesktop.wayland-protocols-unstable — the unstable extension protocols, +-- pre-generated. +-- +-- ───────────────────────────────────────────────────────────────────────── +-- WHY THIS IS A FORK AND NOT AN INLINE DESCRIPTOR +-- +-- wayland-protocols ships **XML and nothing else**: 65 files under `stable/`, +-- `staging/`, `unstable/` and `experimental/`, plus a pkg-config entry +-- naming the directory. A consumer runs `wayland-scanner` over the ones it +-- uses and compiles the result itself — there is no library to link and no +-- header to include until someone generates them. +-- +-- So an inline descriptor has nothing to compile. The generator runs once in +-- [mcpplibs/wayland-protocols](https://github.com/mcpplibs/wayland-protocols) +-- and the output is checked in, which is the same shape as +-- `freedesktop.wayland`'s own protocol code and for the same reason: +-- precomputable output belongs in the repo, not in every consumer's build. +-- No scanner runs when you build against this — `mcpp build` is the whole +-- toolchain — and the fork's CI regenerates with the ECOSYSTEM's +-- `freedesktop.wayland-scanner`, so the generated code cannot drift from the +-- library that marshals it. +-- +-- ───────────────────────────────────────────────────────────────────────── +-- WHY THREE PACKAGES AND NOT ONE +-- +-- Because all 65 in one library DOES NOT LINK, and that was measured rather +-- than feared. `staging/` and `unstable/` carry the same protocol at +-- different maturity levels, and the scanner emits the same symbol names: +-- +-- multiple definition of `zwp_linux_dmabuf_v1_interface' +-- +-- Counted, per exported `wl_interface`: +-- +-- stable n staging = 0 stable 21 exports +-- stable n unstable = 13 staging 76 +-- staging n unstable = 0 unstable 69 +-- within any one tier = 0 +-- +-- The tier is exactly the boundary along which the protocols coexist, and it +-- is upstream's own directory structure rather than a split invented here. A +-- consumer names the tiers it needs; needing two spellings of one protocol is +-- a real conflict, which is what upstream means by shipping XML. +-- +-- THREE UNSTABLE PROTOCOLS ARE NOT SHIPPED, and they ARE that 13-symbol +-- overlap: `xdg-shell-unstable-v5`, `linux-dmabuf-unstable-v1` and +-- `tablet-unstable-v2`. Each was SUPERSEDED by a stable protocol of the same +-- name — upstream keeps the old spelling only for compatibility — so shipping +-- both would make this package unusable beside the stable one it depends on. +-- The choice is forced, not editorial. +-- +-- staging and unstable are NOT self-contained, and that too is measured: both +-- reference `xdg_toplevel_interface`, and staging also +-- `zwp_tablet_tool_v2_interface` — stable defines them. Inside the fork that +-- is a path dependency on the sibling member, so this tarball is +-- self-contained and a consumer does not have to know. +-- +-- ───────────────────────────────────────────────────────────────────────── +-- THE COST OF A WHOLE TIER, MEASURED +-- +-- mcpp links a dependency's objects into the consumer, so naming a tier means +-- carrying it. All 65 protocols compile to 270 KB of `wl_interface` tables — +-- 4 KB each. The bulk of the fork is HEADERS (84,288 lines), and a header +-- costs nothing until it is included. Splitting per protocol would mean 65 +-- packages for 270 KB. +-- +-- `kind = "lib"`, not `"shared"`, and that matches upstream: there is no +-- `libwayland-protocols.so` anywhere, because the marshalling tables are +-- meant to be compiled INTO the program. A shared library would invent an ABI +-- upstream does not have. +package = { + spec = "1", + namespace = "freedesktop", + name = "wayland-protocols-unstable", + description = "wayland-protocols unstable — the zwp_*/zxdg_* protocols still in flux", + licenses = {"MIT"}, + repo = "https://github.com/mcpplibs/wayland-protocols", + type = "package", + + xpm = { + linux = { + -- wayland-protocols' own release number. Upstream tags without a + -- leading v, and so does the fork. + ["1.49"] = { + url = { + GLOBAL = "https://github.com/mcpplibs/wayland-protocols/archive/refs/tags/1.49.tar.gz", + CN = "https://gitcode.com/mcpp-res/wayland-protocols/releases/download/1.49/wayland-protocols-1.49.tar.gz", + }, + sha256 = "0f0f6039b9899699fb3228d5bff25e2a5e5a4792b1fa9964001f73387d7a25e4", + }, + }, + }, + + mcpp = "*/mcpp/unstable/mcpp.toml", +} diff --git a/tests/examples/wayland-protocols/mcpp.toml b/tests/examples/wayland-protocols/mcpp.toml new file mode 100644 index 0000000..e51aa31 --- /dev/null +++ b/tests/examples/wayland-protocols/mcpp.toml @@ -0,0 +1,34 @@ +# wayland-protocols test project. +# +# THIS MEMBER DECLARES ITS OWN [indices]: an index is looked up BY NAMESPACE, +# and the workspace root maps `compat`, so `freedesktop.*` would resolve +# against the published index rather than the packages under test. +[indices] +freedesktop = { path = "../../.." } + +[package] +name = "wayland-protocols-tests" +version = "0.1.0" +standard = "c++23" + +# ONLY staging is named, and stable arrives with it. +# +# staging depends on stable INSIDE the fork, as a path dependency — measured +# necessity, not convention: staging references `xdg_toplevel_interface` and +# `zwp_tablet_tool_v2_interface`, which stable defines. So naming both here is +# an error rather than a redundancy: +# +# dependency freedesktop.wayland-protocols-stable is requested as both a +# version dep (by wayland-protocols-tests) and a path dep (by +# freedesktop.wayland-protocols-staging.49). Pick one. +# +# The rule that falls out: a project that wants staging names staging; one that +# wants only stable names stable. Never both. This member takes the first form, +# and the test then proves that protocols from BOTH tiers link into one +# program — which is the property the three-package split exists to preserve. +# +# stable + unstable is the pair that CANNOT coexist (13 shared symbols). That +# is upstream's semantics rather than a defect, and testing it would mean +# asserting a link failure. +[target.'cfg(linux)'.dependencies.freedesktop] +wayland-protocols-staging = "1.49" diff --git a/tests/examples/wayland-protocols/tests/wayland-protocols.cpp b/tests/examples/wayland-protocols/tests/wayland-protocols.cpp new file mode 100644 index 0000000..19e50ca --- /dev/null +++ b/tests/examples/wayland-protocols/tests/wayland-protocols.cpp @@ -0,0 +1,100 @@ +// wayland-protocols, as a compositor would consume it. +// +// The packages ship the marshalling code wayland-scanner generates from +// upstream's XML. Three things can be wrong and none is a missing symbol: +// +// 1. THE HEADERS ARE THE PUBLIC INTERFACE. A consumer writes +// `#include `, so the generated directory +// has to be EXPOSED rather than build-private. This file compiling is the +// first assertion, and freedesktop.wayland had exactly this bug once — +// masked locally by a header the SubOS happened to carry. +// +// 2. THE INTERFACE TABLES MUST LINK. A header-only package sails past +// compilation; `xdg_wm_base_interface` is a `wl_interface` OBJECT in the +// generated .c, so reading it proves the .c files were compiled. +// +// 3. TWO TIERS MUST COMPOSE. stable and staging are separate packages +// precisely so they can be used together; that they do is what this +// member is for. (stable + unstable is the pair that cannot — 13 shared +// symbols — which is upstream's semantics rather than a defect.) +// +// Nothing here connects: no compositor and no display. + +#ifdef __linux__ + +#include +#include + +#include // stable +#include +#include // stable — the GBM bridge +#include // staging + +#include +#include +#include + +namespace { + +int failures = 0; + +void check(bool ok, const char *what) +{ + std::printf("%-58s %s\n", what, ok ? "ok" : "FAILED"); + if (!ok) { + ++failures; + } +} + +void report(const wl_interface &i, const char *want, const char *tier) +{ + std::printf(" %-8s %-28s version %d, %d method(s)\n", + tier, i.name ? i.name : "(null)", i.version, i.method_count); + check(i.name != nullptr && std::strcmp(i.name, want) == 0, + (std::string(tier) + " " + want + " is linked in").c_str()); + check(i.method_count > 0, "…and it describes its requests"); +} + +} // namespace + +int main() +{ + // ── 1. Both tiers linked, in one program ───────────────────────────── + report(xdg_wm_base_interface, "xdg_wm_base", "stable"); + report(zwp_linux_dmabuf_v1_interface, "zwp_linux_dmabuf_v1", "stable"); + report(wp_cursor_shape_manager_v1_interface, + "wp_cursor_shape_manager_v1", "staging"); + + // ── 2. The server side was generated too ───────────────────────────── + // `xdg_wm_base_send_ping` is a `static inline` only the SERVER header + // defines. Client and server headers are separate scanner outputs and a + // packaging mistake tends to lose one of them. + check(reinterpret_cast(&xdg_wm_base_send_ping) != nullptr, + "the server-side headers were generated too"); + + // ── 3. The library that marshals these is new enough ───────────────── + // The generated code calls wl_proxy_marshal_flags, which arrived in + // libwayland 1.19 — so the protocols and the client library are not from + // different eras. + check(reinterpret_cast(&wl_proxy_marshal_flags) != nullptr, + "libwayland provides wl_proxy_marshal_flags"); + check(reinterpret_cast(&wl_display_create) != nullptr, + "libwayland-server is linked for the compositor side"); + + // ── 4. The GBM bridge protocol is the stable one ───────────────────── + // linux-dmabuf is how a compositor hands a gbm_bo to a client, so it is + // the protocol that ties this package to compat.libgbm. It lives in + // stable/ upstream; the unstable spelling is one of the three this index + // deliberately does not ship. + check(zwp_linux_dmabuf_v1_interface.version >= 4, + "linux-dmabuf is the stable v4+ interface, not the unstable spelling"); + + std::printf("\n%d check(s) failed\n", failures); + return failures == 0 ? 0 : 1; +} + +#else + +int main() { return 0; } + +#endif From ac93354e3c8d2c04fc155e730a1c965efcd10cb3 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Sun, 30 Aug 2026 16:22:24 +0800 Subject: [PATCH 08/17] =?UTF-8?q?compat.pixman=200.46.4:=E8=BD=AF=E4=BB=B6?= =?UTF-8?q?=E5=90=88=E6=88=90=E8=B7=AF=E5=BE=84,SIMD=20=E7=94=A8=20per-glo?= =?UTF-8?q?b=20=E6=A0=87=E5=BF=97=E8=80=8C=E4=B8=8D=E6=98=AF=20fork?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 合成器不是所有东西都交给 GPU —— 损伤区域、光标混合、没有 EGL 时的回退路径,都走 pixman。cairo / X / Mesa 也都坐在它上面。 内联描述符,不需要 fork,而这一点值得说清楚:pixman 上游是**每个指令集一个静态库**, 各自用那个指令集的标志编一个 .c(meson.build:63 的 foreach)。包级 cflags 表达不了 —— "-mssse3" 加到所有文件上,编译器就可能在 pixman-x86.c 的 CPUID 检查**之前**发出 SSSE3 指令,在老 CPU 上是非法指令异常,而这个库的全部设计就是为了避免这件事。 [build] flags 带 glob 正好表达它,而且不是新机制 —— compat.sdl2 早就用它把 -msse3 限定到单个文件。所以既不用 fork 也不用 build.mcpp。与 freedesktop.gldispatch 对照: 那边按架构变的是**编哪些文件**,这边变的是**给哪个文件什么标志**,前者才需要 build.mcpp。 实测确认标志没有外溢: pixman-ssse3.o SSSE3 指令 2 ← 应当有 pixman-sse2.o SSSE3 指令 0 pixman.o SSSE3 指令 0 pixman-fast-path.o SSSE3 指令 0 pixman-x86.o SSSE3 指令 0 踩到一个静默的坑:PIXMAN_API 定义在 pixman-version.h.in 里,不在编译器头里。生成 version.h 时漏掉它,pixman.h 里每个 "PIXMAN_API void pixman_fill(...)" 都会解析成 未知标识符、声明整个丢失 —— 而报出来的错误是某个 SIMD 文件里的 "implicit declaration of function pixman_fill",与真正的原因隔了十万八千里。 测试做真合成:256x256 全部 65536 个像素与合成色一致,外加区域并集运算 —— 后者是 合成器每帧都要调的损伤跟踪,纯 C、无 SIMD。 --- mcpp.toml | 1 + pkgs/c/compat.pixman.lua | 181 +++++++++++++++++++++++++ tests/examples/pixman/mcpp.toml | 11 ++ tests/examples/pixman/tests/pixman.cpp | 119 ++++++++++++++++ 4 files changed, 312 insertions(+) create mode 100644 pkgs/c/compat.pixman.lua create mode 100644 tests/examples/pixman/mcpp.toml create mode 100644 tests/examples/pixman/tests/pixman.cpp diff --git a/mcpp.toml b/mcpp.toml index 2929147..6881fe3 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -87,6 +87,7 @@ members = [ "tests/examples/wayland", "tests/examples/egl", "tests/examples/gl", + "tests/examples/pixman", "tests/examples/wayland-protocols", "tests/examples/libgbm", "tests/examples/libpng", diff --git a/pkgs/c/compat.pixman.lua b/pkgs/c/compat.pixman.lua new file mode 100644 index 0000000..ff9e21f --- /dev/null +++ b/pkgs/c/compat.pixman.lua @@ -0,0 +1,181 @@ +-- compat.pixman — pixman 0.46.4, the pixel-manipulation library. +-- +-- The software compositing path. A Wayland compositor uses it for the surfaces +-- it cannot hand to the GPU — damage regions, cursor blending, the fallback +-- when there is no EGL — and cairo, X and Mesa all sit on it too. +-- +-- ───────────────────────────────────────────────────────────────────────── +-- SHAPE: an inline descriptor, and the SIMD is why that is worth saying +-- +-- pixman is a separable project with its own releases, so by the criterion it +-- is a source build. What made it look like it needed a fork is the SIMD: +-- upstream builds ONE STATIC LIBRARY PER INSTRUCTION SET, each from a single +-- `.c` compiled with that set's flags: +-- +-- foreach simd : simds # meson.build:63 +-- pixman_simd_libs += static_library( +-- 'pixman-' + simd[0], [name + '.c', ...], c_args : simd[2]) +-- +-- Package-wide `cflags` cannot express that. `-mssse3` applied to every file +-- would let the compiler emit SSSE3 in code that runs before the CPUID check +-- in `pixman-x86.c` — an illegal-instruction crash on an older CPU, from a +-- library whose whole design is to dispatch at runtime. +-- +-- `[build] flags` with a `glob` is what expresses it, and this is not a new +-- mechanism: `compat.sdl2` already scopes `-msse3` to one file the same way. +-- So no fork, no `build.mcpp`. Contrast `freedesktop.gldispatch`, where the +-- per-architecture choice is WHICH FILES to compile rather than which flags to +-- give them — that one does need `build.mcpp`. +-- +-- ───────────────────────────────────────────────────────────────────────── +-- WHAT IS COMPILED AND WHAT IS NOT +-- +-- The x86 SIMD (`pixman-sse2.c`, `pixman-ssse3.c`) is in; MMX is not. MMX is +-- 32-bit-x86 era and upstream itself gates it behind `have_mmx`, which is +-- false on x86_64 toolchains. The ARM/MIPS/PPC/RISC-V variants come with +-- assembly files and their own probes; they are absent here for the same +-- reason `freedesktop.egl` leaves X11 out — nothing in this index builds for +-- those targets yet, and the runtime dispatch degrades to the generic path +-- rather than failing. +-- +-- `pixman-config.h` and `pixman-version.h` are meson's `configure_file` +-- outputs. They are generated here rather than probed: every value in them is +-- a property of the target, and the target is Linux with a GCC-compatible +-- toolchain. +package = { + spec = "1", + namespace = "compat", + name = "pixman", + description = "pixman 0.46.4 — low-level pixel manipulation, with runtime-dispatched x86 SIMD", + licenses = {"MIT"}, + repo = "https://gitlab.freedesktop.org/pixman/pixman", + type = "package", + + xpm = { + linux = { + ["0.46.4"] = { + url = { + GLOBAL = "https://gitlab.freedesktop.org/pixman/pixman/-/archive/pixman-0.46.4/pixman-pixman-0.46.4.tar.gz", + CN = "https://gitcode.com/mcpp-res/pixman/releases/download/0.46.4/pixman-0.46.4.tar.gz", + }, + sha256 = "1b8288086e5da0ec5cb95cf174a919cc6fe4548f10dc3cd873b3bb1d9e8fdeab", + }, + }, + }, + + mcpp = { + language = "c++23", + import_std = false, + c_standard = "c11", + + include_dirs = { "*/pixman", "mcpp_generated" }, + + generated_files = { + ["mcpp_generated/pixman-config.h"] = [[ +#ifndef PIXMAN_CONFIG_H +#define PIXMAN_CONFIG_H +/* meson's configure_file output, written out for the one target this + package builds: Linux, x86_64, a GCC-compatible toolchain. */ +#define USE_SSE2 1 +#define USE_SSSE3 1 +#define USE_GCC_INLINE_ASM 1 +#define HAVE_PTHREADS 1 +#define HAVE_POSIX_MEMALIGN 1 +#define HAVE_MMAP 1 +#define HAVE_MPROTECT 1 +#define HAVE_GETPAGESIZE 1 +#define HAVE_SYS_MMAN_H 1 +#define HAVE_UNISTD_H 1 +#define HAVE_FENV_H 1 +#define HAVE_FEDIVBYZERO 1 +#define HAVE_FEENABLEEXCEPT 1 +#define HAVE_BUILTIN_CLZ 1 +#define HAVE_FLOAT128 1 +#define TOOLCHAIN_SUPPORTS_ATTRIBUTE_CONSTRUCTOR 1 +#define TLS __thread +#define PACKAGE "pixman" +#endif +]], + ["mcpp_generated/pixman-version.h"] = [[ +#ifndef PIXMAN_VERSION_H__ +#define PIXMAN_VERSION_H__ +#ifndef PIXMAN_H__ +# error pixman-version.h should only be included by pixman.h +#endif +#define PIXMAN_VERSION_MAJOR 0 +#define PIXMAN_VERSION_MINOR 46 +#define PIXMAN_VERSION_MICRO 4 +#define PIXMAN_VERSION_STRING "0.46.4" +#define PIXMAN_VERSION_ENCODE(major, minor, micro) ( \ + ((major) * 10000) \ + + ((minor) * 100) \ + + ((micro) * 1)) +#define PIXMAN_VERSION PIXMAN_VERSION_ENCODE( \ + PIXMAN_VERSION_MAJOR, \ + PIXMAN_VERSION_MINOR, \ + PIXMAN_VERSION_MICRO) + +/* PIXMAN_API lives HERE upstream, not in a compiler header, and leaving it + out is silent: every `PIXMAN_API void pixman_fill(...)` in pixman.h then + parses as an unknown identifier and the declaration is lost. What surfaces + is `implicit declaration of function 'pixman_fill'` from a SIMD file that + has nothing to do with it. */ +#ifndef PIXMAN_API +# define PIXMAN_API +#endif +#endif +]], + }, + + sources = { + "*/pixman/pixman.c", + "*/pixman/pixman-access.c", + "*/pixman/pixman-access-accessors.c", + "*/pixman/pixman-arm.c", + "*/pixman/pixman-bits-image.c", + "*/pixman/pixman-combine32.c", + "*/pixman/pixman-combine-float.c", + "*/pixman/pixman-conical-gradient.c", + "*/pixman/pixman-edge.c", + "*/pixman/pixman-edge-accessors.c", + "*/pixman/pixman-fast-path.c", + "*/pixman/pixman-filter.c", + "*/pixman/pixman-glyph.c", + "*/pixman/pixman-general.c", + "*/pixman/pixman-gradient-walker.c", + "*/pixman/pixman-image.c", + "*/pixman/pixman-implementation.c", + "*/pixman/pixman-linear-gradient.c", + "*/pixman/pixman-matrix.c", + "*/pixman/pixman-mips.c", + "*/pixman/pixman-noop.c", + "*/pixman/pixman-ppc.c", + "*/pixman/pixman-radial-gradient.c", + "*/pixman/pixman-region16.c", + "*/pixman/pixman-region32.c", + "*/pixman/pixman-region64f.c", + "*/pixman/pixman-riscv.c", + "*/pixman/pixman-solid-fill.c", + "*/pixman/pixman-timer.c", + "*/pixman/pixman-trap.c", + "*/pixman/pixman-utils.c", + "*/pixman/pixman-x86.c", + -- the runtime-dispatched implementations + "*/pixman/pixman-sse2.c", + "*/pixman/pixman-ssse3.c", + }, + + cflags = { "-D_GNU_SOURCE", "-DHAVE_CONFIG_H", "-fPIC" }, + + -- The whole reason this package needs no fork. Each entry gives ONE + -- file the instruction set it implements, so the compiler may emit + -- those instructions there and nowhere else — which is what makes + -- pixman's CPUID dispatch in pixman-x86.c safe. + flags = { + { glob = "*/pixman/pixman-sse2.c", cflags = { "-msse2" } }, + { glob = "*/pixman/pixman-ssse3.c", cflags = { "-mssse3" } }, + }, + + targets = { ["pixman-1"] = { kind = "lib" } }, + }, +} diff --git a/tests/examples/pixman/mcpp.toml b/tests/examples/pixman/mcpp.toml new file mode 100644 index 0000000..51d0bf7 --- /dev/null +++ b/tests/examples/pixman/mcpp.toml @@ -0,0 +1,11 @@ +# pixman test project. +# +# Linux-only like its graphics-stack neighbours; the test compiles to a no-op +# main() elsewhere. +[package] +name = "pixman-tests" +version = "0.1.0" +standard = "c++23" + +[target.'cfg(linux)'.dependencies.compat] +pixman = "0.46.4" diff --git a/tests/examples/pixman/tests/pixman.cpp b/tests/examples/pixman/tests/pixman.cpp new file mode 100644 index 0000000..7584d89 --- /dev/null +++ b/tests/examples/pixman/tests/pixman.cpp @@ -0,0 +1,119 @@ +// compat.pixman — behavioral test. +// +// The interesting thing about this package is not that it links; it is that +// pixman dispatches on the CPU at runtime, and this index compiles the SSE2 +// and SSSE3 implementations with per-file `-msse2` / `-mssse3` so that the +// compiler may emit those instructions THERE and nowhere else. If the flags +// had been package-wide, code running before pixman's own CPUID check could +// carry SSSE3 — an illegal instruction on an older machine, from a library +// whose entire design is to avoid exactly that. +// +// So the test does two things a "does it link" check would not: +// +// * asks pixman which implementation it CHOSE, through a real composite; +// * composites actual pixels and reads them back, because the SIMD paths +// are only reached with enough work to be worth dispatching to. +// +// No display, no GPU, no threads. + +#ifdef __linux__ + +#include + +#include +#include +#include +#include +#include + +namespace { + +int failures = 0; + +void check(bool ok, const char *what) +{ + std::printf("%-58s %s\n", what, ok ? "ok" : "FAILED"); + if (!ok) { + ++failures; + } +} + +} // namespace + +int main() +{ + // ── 1. The library is the version this package says it is ──────────── + std::printf(" pixman %s (header %d.%d.%d)\n", pixman_version_string(), + PIXMAN_VERSION_MAJOR, PIXMAN_VERSION_MINOR, PIXMAN_VERSION_MICRO); + check(pixman_version() == PIXMAN_VERSION, + "the linked library and the header agree on the version"); + check(PIXMAN_VERSION_MAJOR == 0 && PIXMAN_VERSION_MINOR == 46, + "…and it is 0.46, the version the descriptor pins"); + + // ── 2. A real composite, over enough pixels to matter ──────────────── + // 256x256 ARGB: large enough that pixman routes through a fast path + // rather than the trivial one, which is what exercises the dispatched + // implementation the SIMD flags were for. + const int W = 256, H = 256; + std::vector dst(static_cast(W) * H, 0u); + + pixman_color_t colour{}; + colour.red = 0x4000; + colour.green = 0x8000; + colour.blue = 0xc000; + colour.alpha = 0xffff; + pixman_image_t *src = pixman_image_create_solid_fill(&colour); + check(src != nullptr, "pixman_image_create_solid_fill"); + + pixman_image_t *out = pixman_image_create_bits( + PIXMAN_a8r8g8b8, W, H, dst.data(), W * 4); + check(out != nullptr, "pixman_image_create_bits 256x256 a8r8g8b8"); + + if (src != nullptr && out != nullptr) { + pixman_image_composite32(PIXMAN_OP_SRC, src, nullptr, out, + 0, 0, 0, 0, 0, 0, W, H); + + // Every pixel must be the colour that was composited. A dispatch that + // picked a broken implementation shows up here rather than as a crash. + const std::uint32_t want = 0xff4080c0u; + std::size_t wrong = 0; + for (std::uint32_t p : dst) { + if (p != want) { + ++wrong; + } + } + std::printf(" composited %dx%d, first pixel 0x%08x (wanted 0x%08x)\n", + W, H, dst[0], want); + check(wrong == 0, "every pixel is the colour that was composited"); + } + + // ── 3. Region arithmetic, the other half of what a compositor uses ─── + // Damage tracking is pixman_region32, and it is pure C — no SIMD, but it + // is the part a Wayland compositor calls on every frame. + { + pixman_region32_t a, b, r; + pixman_region32_init_rect(&a, 0, 0, 100, 100); + pixman_region32_init_rect(&b, 50, 50, 100, 100); + pixman_region32_init(&r); + check(pixman_region32_union(&r, &a, &b) != 0, "pixman_region32_union"); + const pixman_box32_t *e = pixman_region32_extents(&r); + std::printf(" union extents: (%d,%d)-(%d,%d)\n", e->x1, e->y1, e->x2, e->y2); + check(e->x1 == 0 && e->y1 == 0 && e->x2 == 150 && e->y2 == 150, + "…and the extents are the union of the two rects"); + pixman_region32_fini(&a); + pixman_region32_fini(&b); + pixman_region32_fini(&r); + } + + if (src != nullptr) { pixman_image_unref(src); } + if (out != nullptr) { pixman_image_unref(out); } + + std::printf("\n%d check(s) failed\n", failures); + return failures == 0 ? 0 : 1; +} + +#else + +int main() { return 0; } + +#endif From 30b5b6ded26b05d19b691a8cbceaa73c9912e702 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Sun, 30 Aug 2026 16:25:48 +0800 Subject: [PATCH 09/17] =?UTF-8?q?docs:=20=E5=AE=9E=E7=8E=B0=E8=AE=B0?= =?UTF-8?q?=E5=BD=95=E4=B8=8E=E5=AF=B9=E8=AE=BE=E8=AE=A1=E7=9A=84=E5=9B=9B?= =?UTF-8?q?=E5=A4=84=E6=9B=B4=E6=AD=A3,=E6=B2=99=E7=AE=B1=E9=AA=8C?= =?UTF-8?q?=E8=AF=81=E6=89=A9=E5=88=B0=E6=B8=B2=E6=9F=93=E9=93=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 设计写完就动手了,§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,能初始化不等于能画。 --- ...26-08-30-graphics-stack-coverage-design.md | 169 ++++++++++++++++++ tests/verify_graphics_closed_loop_sandbox.sh | 51 +++++- 2 files changed, 216 insertions(+), 4 deletions(-) diff --git a/.agents/docs/2026-08-30-graphics-stack-coverage-design.md b/.agents/docs/2026-08-30-graphics-stack-coverage-design.md index cb95653..120dfd5 100644 --- a/.agents/docs/2026-08-30-graphics-stack-coverage-design.md +++ b/.agents/docs/2026-08-30-graphics-stack-coverage-design.md @@ -390,3 +390,172 @@ G6 libudev 路线决策 → libseat ← 最后,单独评估 > **G1a 三个成员是分水岭 —— 做完它,mcpp 从「EGL 能初始化」变成「能画」; > 其余都是补齐,唯一的架构性问题是 Vulkan 的 `XDG_DATA_DIRS` 目录合并, > 而那一行改在 xim-pkgindex,不在这里。** + +--- + +## 10. 实现结果与对本设计的更正(2026-08-30 执行记录) + +设计写完就动手了。这一节记录**做出来的东西**,以及**设计里被实现推翻的部分** —— +后者更重要,因为每一条都是被实测否掉的。 + +### 10.1 交付 + +| # | 交付 | 状态 | +|---|---|---| +| G1a | mcpplibs/libglvnd 加 `mcpp/{glesv2,glesv1,opengl}` 三个成员 | `v1.7.0`,CI 全绿 | +| G1b | `khronos.{glesv2,glesv1,opengl}` 模块层 | 同上,与 C 库一起做完 | +| G2 | xim-pkgindex `nvidia-gl-host-link` 0.1.3 声明 Vulkan ICD | PR #731,已实测 | +| G3 | **mcpplibs/wayland-protocols**(新建)三个 tier | `1.49`,CI 待绿 | +| G4a | `compat.pixman` 0.46.4 | 已实测 | +| 索引 | `freedesktop.{glesv2,glesv1,opengl}`、三个 `wayland-protocols-*`、`compat.pixman` | PR #294 | +| 镜像 | libglvnd / wayland-protocols / pixman 三份 CN,均已核对 sha256 | 完成 | + +### 10.2 被推翻的:G3 的「新增仓 0」 + +设计 §6 写着「新增仓数量 = 0 —— G1 复用 libglvnd,G3 做 wayland fork 的第五个成员」。 +**G3 那半是错的。** + +`mcpplibs/wayland` 的 `upstream/` 是 wayland **1.26.0** 且 CI 逐字节 diff; +wayland-protocols 是**另一个上游项目、另一个版本(1.49)**。一个仓一个上游一个版本, +所以它必须是独立 fork。**新增仓数量 = 1。** + +### 10.3 被推翻的:G3 的「一个包装全部 65 个协议」 + +设计说「全量预生成」,只提了体积。**体积不是问题,链接才是。** + +``` +multiple definition of `zwp_linux_dmabuf_v1_interface' +``` + +staging/ 与 unstable/ 携带**同一个协议的不同成熟度**,scanner 为两者生成**同名符号**。 +按导出的 `wl_interface` 数清点: + +| | | +|---|---| +| stable ∩ staging | **0** | +| stable ∩ unstable | **13** | +| staging ∩ unstable | **0** | +| 任一 tier 内部 | **0** | + +所以是**三个包,按 tier 分** —— 而这个边界是上游自己的目录结构,不是发明出来的。 + +由此还带出两条只有动手才会发现的事: + +1. **三个 unstable 协议不能发**:`xdg-shell-unstable-v5`、`linux-dmabuf-unstable-v1`、 + `tablet-unstable-v2` —— 它们**就是**那 13 个重叠符号,而且各自都已被同名 stable 协议 + 取代。上游留着只为兼容,一个包不可能两个都发。 +2. **staging/unstable 依赖 stable**,实测:两者都引用 `xdg_toplevel_interface`,staging + 还引用 `zwp_tablet_tool_v2_interface`。fork 内用 path 依赖,于是**消费者不能同时写 + staging 和 stable** —— mcpp 报「既是 version dep 又是 path dep」。规则:要 staging + 就只写 staging。 + +体积那一问也量了,而且答案与担心相反:65 个协议的 `.c` 一共 **5910 行,编出 270KB**; +3.5MB 几乎全是**头文件**,不 include 就不花钱。 + +### 10.4 被推翻两次的:G2 的形态 + +设计 §3 已经把 `VK_DRIVER_FILES` 标成「作废,应当接既有的共享目录机制」。**接完发现两者 +是互补的,不是二选一** —— 但第二个仍然**现在不能做**。 + +实测三段: + +``` +A 改动前 devices = 1 → llvmpipe (LLVM 20.1.2) +B sentinel 声明 Vulkan ICD devices = 3 → NVIDIA RTX 4080 ×2 + llvmpipe +C 再加 VK_DRIVER_FILES devices = 1 → NVIDIA RTX 4080 +``` + +B 是本轮做的:GPU 到位了。但 NVIDIA **出现两次** —— sentinel 补上 `libXext` 之后, +宿主 `/usr/share` 里那份裸 soname 的 ICD 也能加载了。C 能消掉重复(而且 +`VK_DRIVER_FILES` **接受目录**,一行 DISCOVERY 就够)。 + +**C 现在不能做**:`xim:mesa` 的 payload 只带 `radeon_icd.x86_64.json` 一个 ICD。声明 +`VK_DRIVER_FILES` 会让生态集合成为唯一权威,而在 Intel 机器上那个集合是空的 —— 等于拿 +「NVIDIA 重复枚举」换「Intel 上完全没有 Vulkan」。前置条件是 mesa 把它构建的 ICD 都发 +出来(anv / lvp / nouveau),那是 payload 的问题。 + +### 10.5 被修正的:G4 pixman「SIMD 自带门控」 + +设计说 pixman 的 SIMD「自带 `#ifdef` 门控,不需要 build.mcpp」。**分类(A 类内联)对了, +理由错了。** + +文件确实自带门控,但**编译标志是逐文件的**:上游每个指令集建一个静态库,各自带 +`-msse2` / `-mssse3`。包级 cflags 表达不了 —— `-mssse3` 加到所有文件上,编译器就可能在 +`pixman-x86.c` 的 CPUID 检查**之前**发出 SSSE3 指令。 + +真正的答案是 **`[build] flags` 带 `glob`**,而且不是新机制:`compat.sdl2` 早就用它把 +`-msse3` 限定到单个文件。实测标志没有外溢: + +``` +pixman-ssse3.o SSSE3 指令 2 ← 应当有 +pixman-sse2.o SSSE3 指令 0 +pixman.o SSSE3 指令 0 +pixman-fast-path.o SSSE3 指令 0 +pixman-x86.o SSSE3 指令 0 +``` + +**由此得到一条判据补充**,值得记住,因为它把 build.mcpp 的边界又划细了一层: + +> 按目标变的是**编哪些文件** → `build.mcpp`(GLdispatch 的 entry stub)。 +> 按目标变的是**给哪个文件什么标志** → `[build] flags` 的 `glob`(pixman 的 SIMD)。 + +### 10.6 G1 的三个坑(都已写进代码注释) + +1. **`genglmod.sh` 必须 `LC_ALL=C`。** CI 上重新生成得到**一样的符号数**(358/145/653) + 却有 48 行 glesv2 和 88 行 opengl 不同 —— `sort` 按 locale 排序混合大小写标识符, + 开发机 en_US.UTF-8 与 runner 的 C 顺序不同。同样的输入,不同的文件。 +2. **包内测试无法证明 `.so` 身份。** 它链接的是本包的**对象**,所以 test 二进制自己定义 + `glClear`,dladdr 报的是可执行文件 —— 一条「不是 payload 的副本」的断言会在从未看过 + 任何库的情况下通过。`.so` 级身份检查属于**消费者**,在 mcpp-index 的成员里。 +3. **三个 GL flavour 符号面重叠**(都导出 `glClear`)。同一进程链两个,名字绑到先加载的 + 那个 —— 实测:一个想用 GLESv2 的程序里 `glClear` 落到了 `libGLESv1_CM.so.1`,毫无 + 提示。所以索引测试成员只依赖一个 flavour,并断言 `glClear` 确实解析在它里面。 + +模块层那个「GL 入口是否外部链接」的风险**解除了**:`KHRONOS_APICALL` 在 Linux 分支展开 +为空,所以 `GL_APICALL void GL_APIENTRY glClear(...)` 就是普通外部链接声明,和 EGL 一样 +不需要 forwarder。 + +### 10.7 pixman 的一个静默坑 + +`PIXMAN_API` 定义在 **`pixman-version.h.in`** 里,不在编译器头里。生成 version.h 时漏掉 +它,`pixman.h` 里每个 `PIXMAN_API void pixman_fill(...)` 都解析成未知标识符、声明整个 +丢失 —— 而报出来的错误是某个 SIMD 文件里的 +`implicit declaration of function 'pixman_fill'`,与真正的原因隔了十万八千里。 + +### 10.8 沙箱验证已扩到渲染链,并重跑 + +签进仓的 `tests/verify_graphics_closed_loop_sandbox.sh` 加了 GLES2 渲染,在合成 home、 +清空 store、从零构建的沙箱里对分支重跑(`--sandbox --gpu`): + +``` +===== 6. did anything come from the host? ===== + PASS: the host.s copies were present and reachable, and none of them won + +===== 7. run it ===== + /dev/dri/renderD128 drm driver nvidia-drm + eglInitialize EGL 1.5, vendor Mesa Project + GL_VERSION OpenGL ES 3.2 Mesa 25.0.7 + glReadPixels 64 128 191 255 (wanted 64 128 191 255) + /dev/dri/card0 drm driver simpledrm + gbm_bo_create 256x256 stride=1024 + GL_VERSION OpenGL ES 3.2 Mesa 25.0.7 + glReadPixels 64 128 191 255 (wanted 64 128 191 255) + + reached EGL on a real device: yes + drew and read the pixel back: yes +RESULT: PASS +``` + +**这是本轮之前做不到的那一步。** §19.4 那次止于 `eglInitialize` —— 能初始化不等于能画。 +现在闭环从「打开 DRM 节点」一路走到「读回自己画的像素」,而且是在宿主图形库在场且可达 +的沙箱里。 + +### 10.9 仍未做 + +- **G4b `libxkbcommon`**:独立项目,内联描述符可行,但运行期还需要 `xkeyboard-config` + 的**数据文件**,那是第二个包(纯数据)。 +- **G5 `libinput`**:拖 `libevdev` / `mtdev` / `libudev`。 +- **G6 `libudev` / `libseat`**:systemd 邻域,设计里就写明「先不做,单独评估」。 + +前两条是普通工作量;G6 是需要决策的。合成器可以在「已有 DRM master」的前提下开发 +(从 TTY 直接启动、或 `SEATD_SOCK`),把 session 管理留到最后。 diff --git a/tests/verify_graphics_closed_loop_sandbox.sh b/tests/verify_graphics_closed_loop_sandbox.sh index 07fadcd..502de41 100755 --- a/tests/verify_graphics_closed_loop_sandbox.sh +++ b/tests/verify_graphics_closed_loop_sandbox.sh @@ -95,6 +95,7 @@ libgbm = "25.0.7" [target.'cfg(linux)'.dependencies.freedesktop] egl = "1.7.0" +glesv2 = "1.7.0" wayland = "1.26.0" wayland-server = "1.26.0" TOML @@ -113,7 +114,10 @@ cat > "$W/src/main.cpp" <<'CPP' #include #include +#include + import khronos.egl; +import khronos.glesv2; import freedesktop.wayland.client; import freedesktop.wayland.server; @@ -129,7 +133,7 @@ int main() } else { std::puts(" wl_display_create FAILED"); return 1; } std::puts(" -- DRM node -> GBM device -> EGL display --"); - int reached = 0; + int reached = 0, drew = 0; for (const char *node : {"/dev/dri/renderD128", "/dev/dri/card0"}) { int fd = ::open(node, O_RDWR); if (fd < 0) { std::printf(" %-22s (no access)\n", node); continue; } @@ -155,6 +159,44 @@ int main() std::printf(" eglInitialize EGL %d.%d, vendor %s\n", ma, mi, eglQueryString(d, EGL_VENDOR)); reached = 1; + + // AND THEN DRAW. Reaching eglInitialize only proves the + // dispatch is live; a stack that cannot render gets this + // far too. A surfaceless context (GBM has no pbuffer + // configs) into an FBO, clear it, read the pixel back. + eglBindAPI(EGL_OPENGL_ES_API); + EGLint ca[] = { EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT, + EGL_RED_SIZE, 8, EGL_GREEN_SIZE, 8, + EGL_BLUE_SIZE, 8, EGL_ALPHA_SIZE, 8, EGL_NONE }; + EGLConfig cfg{}; EGLint n = 0; + EGLint xa[] = { EGL_CONTEXT_CLIENT_VERSION, 2, EGL_NONE }; + if (eglChooseConfig(d, ca, &cfg, 1, &n) && n > 0) { + EGLContext ctx = eglCreateContext(d, cfg, EGL_NO_CONTEXT, xa); + if (ctx != EGL_NO_CONTEXT && + eglMakeCurrent(d, EGL_NO_SURFACE, EGL_NO_SURFACE, ctx)) { + GLuint rb = 0, fbo = 0; + glGenRenderbuffers(1, &rb); + glBindRenderbuffer(GL_RENDERBUFFER, rb); + glRenderbufferStorage(GL_RENDERBUFFER, GL_RGBA8, 64, 64); + glGenFramebuffers(1, &fbo); + glBindFramebuffer(GL_FRAMEBUFFER, fbo); + glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, + GL_RENDERBUFFER, rb); + std::printf(" GL_VERSION %s\n", glGetString(GL_VERSION)); + glClearColor(0.25f, 0.5f, 0.75f, 1.0f); + glClear(GL_COLOR_BUFFER_BIT); + glFinish(); + unsigned char px[4] = {0,0,0,0}; + glReadPixels(32, 32, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, px); + std::printf(" glReadPixels %u %u %u %u (wanted 64 128 191 255)\n", + px[0], px[1], px[2], px[3]); + drew = (px[0] == 64 && px[1] == 128 && px[2] == 191 && px[3] == 255); + glDeleteFramebuffers(1, &fbo); + glDeleteRenderbuffers(1, &rb); + eglMakeCurrent(d, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); + } + if (ctx != EGL_NO_CONTEXT) eglDestroyContext(d, ctx); + } eglTerminate(d); } } @@ -163,6 +205,7 @@ int main() ::close(fd); } std::printf("\n reached EGL on a real device: %s\n", reached ? "yes" : "no"); + std::printf(" drew and read the pixel back: %s\n", drew ? "yes" : "no"); return 0; } CPP @@ -175,14 +218,14 @@ say "5. what the loader actually resolved" BIN=$(find "$W/target" -name closed-loop -type f -perm -u+x | head -1) [ -n "$BIN" ] || { echo "FAIL: no binary"; exit 1; } I=$(readelf -p .interp "$BIN" | grep -oE '/[^ ]*ld-linux[^ ]*') -"$I" --list "$BIN" | grep -E 'libEGL|libGLdispatch|libgbm|libdrm|libwayland|libffi|libexpat' \ +"$I" --list "$BIN" | grep -E 'libEGL|libGLESv2|libGLdispatch|libgbm|libdrm|libwayland|libffi|libexpat' \ | sed "s|$SUBOS||g; s|$W||g" say "6. did anything come from the host?" -if "$I" --list "$BIN" | grep -E 'libEGL|libGLdispatch|libgbm|libdrm|libwayland|libffi|libexpat' \ +if "$I" --list "$BIN" | grep -E 'libEGL|libGLESv2|libGLdispatch|libgbm|libdrm|libwayland|libffi|libexpat' \ | grep -qE '=> /(usr/)?lib/'; then echo " FAIL: a graphics library resolved to the host" - "$I" --list "$BIN" | grep -E 'libEGL|libGLdispatch|libgbm|libdrm|libwayland' | grep -E '=> /(usr/)?lib/' + "$I" --list "$BIN" | grep -E 'libEGL|libGLESv2|libGLdispatch|libgbm|libdrm|libwayland' | grep -E '=> /(usr/)?lib/' exit 1 fi echo " PASS: the host's copies were present and reachable, and none of them won" From e36c6a1b76a244f3e771946307cab5eee76860a8 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Sun, 30 Aug 2026 16:27:17 +0800 Subject: [PATCH 10/17] =?UTF-8?q?docs:=20=C2=A720=20=E7=9A=84=E7=BC=BA?= =?UTF-8?q?=E5=8F=A3=E8=A1=A8=E6=A0=87=E6=B3=A8=E4=B8=BA=E5=B7=B2=E8=A2=AB?= =?UTF-8?q?=E5=90=8E=E7=BB=AD=E5=B7=A5=E4=BD=9C=E6=94=B9=E5=86=99,?= =?UTF-8?q?=E5=B9=B6=E7=BB=99=E5=87=BA=E7=8E=B0=E7=8A=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 那张表是 2026-08-30 上午盘点时的状态,当天下午 G1/G2/G3/G4a 都实现了 —— 渲染链 (GLESv2)、wayland-protocols 三个 tier、pixman 都已进索引。表里留着盘点时的判断 是有价值的(它是当时的真实结论),但不标注就会误导,所以并排给出现状并指向 覆盖面设计 §10。 仍缺的是输入链:libxkbcommon、libinput、libudev/libseat。 --- .agents/docs/2026-08-30-gbm-cross-repo-closed-loop-plan.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.agents/docs/2026-08-30-gbm-cross-repo-closed-loop-plan.md b/.agents/docs/2026-08-30-gbm-cross-repo-closed-loop-plan.md index f46b987..6b97a39 100644 --- a/.agents/docs/2026-08-30-gbm-cross-repo-closed-loop-plan.md +++ b/.agents/docs/2026-08-30-gbm-cross-repo-closed-loop-plan.md @@ -1419,7 +1419,7 @@ issue #527 问的是「在系统级开发(Wayland 合成器、Mesa/Vulkan 扩展 | 场景 | 结论 | 依据 | |---|---|---| | **GBM 显存管理** | ✅ **完全覆盖** | §19.4 实测:`gbm_create_device` + `gbm_bo_create` 真分配 256x256,拿到驱动自己的 stride;宿主 `libgbm.so.1` 在场可达却没被选中 | -| **Wayland 合成器** | ⚠️ **只覆盖了协议库那一层** | 协议库(client/server/scanner/util)+ EGL + GBM + DRM 都有;但**渲染链和输入链整条缺**,见 20.2 | +| **Wayland 合成器** | ⚠️ 盘点时只覆盖协议库那一层;**当天下午补齐了渲染链**(GLESv2 + wayland-protocols + pixman),**输入链仍缺**(libxkbcommon / libinput / libseat) | 见 20.2 与覆盖面设计 §10 | | **Mesa/Vulkan 扩展** | ❌ **Vulkan 侧仍伸手够宿主** | `compat.vulkan-runtime` 明写着它把宿主 `/usr/lib` 的 ICD 做成符号链接农场,是这个栈里最后一处 host 边 | **所以对 #527 的诚实回答是**:`-lgbm` 那个具体诉求,推荐方式**完全能覆盖且已证明**; From 89e6be584ecbff3a6fb70d741ac0e9c3c74c2980 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Sun, 30 Aug 2026 16:28:02 +0800 Subject: [PATCH 11/17] =?UTF-8?q?docs:=20=C2=A720=20=E7=9A=84=E7=BC=BA?= =?UTF-8?q?=E5=8F=A3=E8=A1=A8=E6=A0=87=E6=B3=A8=E4=B8=BA=E5=B7=B2=E8=A2=AB?= =?UTF-8?q?=E5=90=8E=E7=BB=AD=E5=B7=A5=E4=BD=9C=E6=94=B9=E5=86=99,?= =?UTF-8?q?=E5=B9=B6=E5=88=97=E5=87=BA=E7=8E=B0=E7=8A=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 那张表是当天上午盘点时的状态,下午 G1/G2/G3/G4a 都实现了。左列保留不是懒 —— 「当时缺什么」这个判断本身准确,而且它决定了后来做什么;右列给现状并指向覆盖面 设计 §10 的实现记录。 结论也从「渲染链和输入链都缺」变成「缺的是输入链」:libxkbcommon、libinput、 libudev/libseat。 --- ...6-08-30-gbm-cross-repo-closed-loop-plan.md | 27 ++++++++++++------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/.agents/docs/2026-08-30-gbm-cross-repo-closed-loop-plan.md b/.agents/docs/2026-08-30-gbm-cross-repo-closed-loop-plan.md index 6b97a39..bb4713a 100644 --- a/.agents/docs/2026-08-30-gbm-cross-repo-closed-loop-plan.md +++ b/.agents/docs/2026-08-30-gbm-cross-repo-closed-loop-plan.md @@ -1427,18 +1427,25 @@ issue #527 问的是「在系统级开发(Wayland 合成器、Mesa/Vulkan 扩展 ### 20.2 Wayland 合成器缺什么(逐个点名) -一个真实合成器(wlroots / weston 量级)需要的,索引里的实际情况: +一个真实合成器(wlroots / weston 量级)需要的,索引里的实际情况。 -| 需要 | 索引里 | 性质 | +> ⚠ **这张表是 2026-08-30 上午盘点时的状态,当天下午被自己的后续工作改写了一半。** +> 右列是现状;实现记录与踩到的坑见 +> [`2026-08-30-graphics-stack-coverage-design.md`](2026-08-30-graphics-stack-coverage-design.md) §10。 +> 保留左列不是懒,是因为「当时缺什么」这个判断本身准确,而它决定了后来做什么。 + +| 需要 | 盘点时 | 现在 | |---|---|---| -| `libwayland-server` / `-client` / `-scanner` | ✅ 有,源码构建 | 已完成 | -| EGL、GBM、libdrm | ✅ 有 | 已完成 | -| **`libGLESv2`** | ❌ **没有** | ⚠ **最要命的一个** | -| `wayland-protocols`(xdg-shell 等 XML) | ❌ 没有 | 没有它连基本桌面 shell 都实现不了 | -| `libxkbcommon` | ❌ 没有 | 键盘 | -| `libinput` | ❌ 没有 | 输入 | -| `libudev` / `libseat` | ❌ 没有 | 设备枚举、session / DRM master | -| `pixman` | ❌ 没有 | 软件合成路径 | +| `libwayland-server` / `-client` / `-scanner` | ✅ 有,源码构建 | ✅ | +| EGL、GBM、libdrm | ✅ 有 | ✅ | +| **`libGLESv2`** | ❌ **没有**(最要命的一个) | ✅ `freedesktop.glesv2`,连同 glesv1 / opengl | +| `wayland-protocols`(xdg-shell 等 XML) | ❌ 没有 | ✅ 三个 tier 包 stable / staging / unstable | +| `pixman` | ❌ 没有 | ✅ `compat.pixman` | +| `libxkbcommon` | ❌ 没有 | ❌ **仍缺**(键盘;还需 xkeyboard-config 数据包) | +| `libinput` | ❌ 没有 | ❌ **仍缺**(输入;拖 libevdev / mtdev / libudev) | +| `libudev` / `libseat` | ❌ 没有 | ❌ **仍缺**,需单独决策 | + +**所以现在缺的是输入链,不再是渲染链。** **`libGLESv2` 为什么最要命**:合成器是通过 EGL 拿 context、然后用 **GLES2** 画的。 这一轮建出了 `libEGL.so.1`,但 libglvnd 的 GL/GLES 系列(`libGL`、`libGLX`、`libOpenGL`、 From eb41eb21b674f3f83ee0dba0ce6796fade7896f2 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Sun, 30 Aug 2026 16:46:30 +0800 Subject: [PATCH 12/17] =?UTF-8?q?=E8=BE=93=E5=85=A5=E9=93=BE=E7=9A=84?= =?UTF-8?q?=E4=B8=8B=E4=B8=89=E5=B1=82:compat.mtdev=E3=80=81compat.libudev?= =?UTF-8?q?=E3=80=81freedesktop.libevdev?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit libinput 链这三个,所以合成器的输入链从它们开始。 compat.mtdev(1.1.7)是判据最简单的那一档:五个 C 文件、三个公开头、没有生成物、 Linux 上没有需要探测的配置。内联描述符。 compat.libudev 用的是 **libudev-zero 1.0.5**,不是 systemd 的也不是 eudev 的。三个 实现里只有它同时【活着】且【可独立分发】:systemd 的不可分离,eudev 是 Gentoo 的 fork 但 2021 年就停更了。libudev-zero 直接读 /sys 而不是跟 udevd 说话 —— 这在 subos 里正是对的,因为那里没有 udevd,一个需要守护进程的实现只能在开发者机器上跑。 代价写在描述符里:没有 netlink 热插拔,udev_monitor 只给打开时 /sys 里的设备。 kind = "shared" 且用【规范 soname】libudev.so.1,是有意的:宿主的 libinput 或 payload 一旦已经链了这个 soname,就会复用链接图里的那一份 —— 与 compat.libdrm 依赖的 是同一条 soname 复用性质 —— 于是进程里恰好有一个 libudev,而且是这一个。 USB_IDS_PATH 定义为【空】。上游指向 /share/hwdata/usb.ids,重定位后那是宿主的 文件;与 libgbm 的后端路径、libglvnd 的 vendor 目录同形,答案也一样。上游自己就是优雅 降级的(udev.c:95 打不开就 return 0),而 libinput 需要的【枚举】根本不碰它。 freedesktop.libevdev 是 fork,因为 event-names.h 是生成的 —— 1692 行,Python 脚本。 而且生成的确定性【只来自上游自带内核头】:meson.build:43 用的是 tarball 里那份 linux/input.h,不是 /usr/include。实测宿主头会给出不同的表(1664 行对 1692), 也就是说 libevdev_event_code_get_name 的答案会取决于编译它的机器。fork 的 CI 也按 自带头重新生成再 diff,理由相同。 三个测试都做真事:mtdev 走 slot 状态机,libudev 从 /sys 枚举到 20 个真实输入设备 并断言加载的不是宿主的那份,libevdev 查五张不同的生成表并做反向名字解析。 --- mcpp.toml | 3 + pkgs/c/compat.libudev.lua | 127 +++++++++++++++++++++ pkgs/c/compat.mtdev.lua | 62 ++++++++++ pkgs/f/freedesktop.libevdev.lua | 53 +++++++++ tests/examples/libevdev/mcpp.toml | 14 +++ tests/examples/libevdev/tests/libevdev.cpp | 78 +++++++++++++ tests/examples/libudev/mcpp.toml | 7 ++ tests/examples/libudev/tests/libudev.cpp | 98 ++++++++++++++++ tests/examples/mtdev/mcpp.toml | 7 ++ tests/examples/mtdev/tests/mtdev.cpp | 65 +++++++++++ 10 files changed, 514 insertions(+) create mode 100644 pkgs/c/compat.libudev.lua create mode 100644 pkgs/c/compat.mtdev.lua create mode 100644 pkgs/f/freedesktop.libevdev.lua create mode 100644 tests/examples/libevdev/mcpp.toml create mode 100644 tests/examples/libevdev/tests/libevdev.cpp create mode 100644 tests/examples/libudev/mcpp.toml create mode 100644 tests/examples/libudev/tests/libudev.cpp create mode 100644 tests/examples/mtdev/mcpp.toml create mode 100644 tests/examples/mtdev/tests/mtdev.cpp diff --git a/mcpp.toml b/mcpp.toml index 6881fe3..150e4fc 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -87,6 +87,9 @@ members = [ "tests/examples/wayland", "tests/examples/egl", "tests/examples/gl", + "tests/examples/libevdev", + "tests/examples/libudev", + "tests/examples/mtdev", "tests/examples/pixman", "tests/examples/wayland-protocols", "tests/examples/libgbm", diff --git a/pkgs/c/compat.libudev.lua b/pkgs/c/compat.libudev.lua new file mode 100644 index 0000000..5ef7965 --- /dev/null +++ b/pkgs/c/compat.libudev.lua @@ -0,0 +1,127 @@ +-- compat.libudev — libudev-zero 1.0.5, a libudev without systemd. +-- +-- `libinput` needs libudev to enumerate input devices and to hear about +-- hotplug. Upstream libudev is part of systemd, which an ecosystem that builds +-- its own userspace cannot take: systemd is not separable, its build wants a +-- large slice of the distribution, and the result would be a package that +-- exists to provide 5,000 lines of device enumeration. +-- +-- ───────────────────────────────────────────────────────────────────────── +-- WHY libudev-zero AND NOT eudev OR systemd +-- +-- Three implementations of this ABI exist: +-- +-- systemd's the original; not separable, see above +-- eudev Gentoo's fork of the systemd code; UNMAINTAINED since 2021 +-- libudev-zero a from-scratch reimplementation of the same ABI, MIT, +-- five C files, no daemon at all +-- +-- The third is the only one that is both alive and separable. It reads +-- /sys directly instead of talking to a udev daemon, which is exactly right +-- here: a subos has no udevd, and a program that needed one would work on the +-- developer's machine and nowhere else. +-- +-- WHAT IT DOES NOT DO, and it matters: no hotplug MONITOR over netlink from +-- udevd — `udev_monitor` returns devices from /sys at open time. libinput +-- degrades to "the devices present at startup", which is correct for a +-- compositor brought up on a fixed machine and wrong for hot-plugging a +-- keyboard mid-session. Named here rather than discovered later. +-- +-- ───────────────────────────────────────────────────────────────────────── +-- SHAPE: `kind = "shared"` with the CANONICAL soname, deliberately +-- +-- `libudev.so.1` is the soname systemd's carries, and this package uses it on +-- purpose. A host libinput or a payload that already links libudev.so.1 will +-- REUSE whatever is in the link map — the same soname-reuse property +-- compat.libdrm relies on — so a process ends up with exactly one libudev and +-- it is this one. Merged in as objects instead, a consumer and a dependency +-- would each get their own copy of the device list. +package = { + spec = "1", + namespace = "compat", + name = "libudev", + description = "libudev-zero 1.0.5 — the libudev ABI without systemd or a daemon, for libinput", + licenses = {"ISC"}, + repo = "https://github.com/illiliti/libudev-zero", + type = "package", + + xpm = { + linux = { + -- libudev-zero's own version. It reports itself as udev 251 + -- through `udev_get_version`, which is the ABI level it targets + -- rather than anything about this package. + ["1.0.5"] = { + url = { + GLOBAL = "https://github.com/illiliti/libudev-zero/archive/refs/tags/1.0.5.tar.gz", + CN = "https://gitcode.com/mcpp-res/libudev-zero/releases/download/1.0.5/libudev-zero-1.0.5.tar.gz", + }, + sha256 = "bf4372f79ddbe6b0e266a3d2994ffac7018a7edf4f87632aecb5176565d96138", + }, + }, + }, + + mcpp = { + language = "c++23", + import_std = false, + c_standard = "c11", + + include_dirs = { "*", "mcpp_generated" }, + + -- Upstream's header is `udev.h`; it becomes `libudev.h` at INSTALL + -- time (Makefile's install-headers), which a package with no install + -- step does not get for free. Every consumer in the world writes + -- `#include ` — that is the name systemd's libudev ships — + -- so a forwarding header carries it. Same shape as compat.libdrm's + -- per-directory forwarders, and for the same reason: the include + -- SPELLING is part of the interface. + -- + -- A LITERAL, not a `..` concatenation: the descriptor parser reads + -- `generated_files` as data and does not execute Lua, so a + -- concatenated value fails with `malformed mcpp segment near key + -- 'define'` — an error naming a key that is not the problem. + generated_files = { + ["mcpp_generated/libudev.h"] = [[ +#ifndef MCPP_LIBUDEV_FORWARD_H +#define MCPP_LIBUDEV_FORWARD_H +#include +#endif +]], + }, + + sources = { + "*/udev.c", + "*/udev_device.c", + "*/udev_enumerate.c", + "*/udev_list.c", + "*/udev_monitor.c", + }, + + cflags = { + "-D_GNU_SOURCE", + -- The USB vendor/product name database, and deliberately EMPTY. + -- + -- Upstream points this at `/share/hwdata/usb.ids`, which + -- after relocation means the HOST's file. Same shape as libgbm's + -- compiled-in backend path and libglvnd's vendor directory, and it + -- gets the same answer: an empty default, so a missing database + -- surfaces as "no USB names" rather than as silently reading a + -- host file into a sandboxed process. + -- + -- The degradation is graceful by upstream's own design: + -- `usb_ids_lookup_vendor` opens the file and `return 0` if it + -- cannot (udev.c:95). Device ENUMERATION — what libinput actually + -- needs — never touches it; what is lost is the human-readable + -- vendor string on a USB device. + -- + -- Nothing in this index ships hwdata yet. When something does, + -- this becomes a path the environment declares, exactly like + -- GBM_BACKENDS_PATH. + [[-DUSB_IDS_PATH=\"\"]], + "-fPIC", + }, + + targets = { + ["udev"] = { kind = "shared", soname = "libudev.so.1" }, + }, + }, +} diff --git a/pkgs/c/compat.mtdev.lua b/pkgs/c/compat.mtdev.lua new file mode 100644 index 0000000..19f8c1f --- /dev/null +++ b/pkgs/c/compat.mtdev.lua @@ -0,0 +1,62 @@ +-- compat.mtdev — mtdev 1.1.7, the multitouch protocol translator. +-- +-- Kernel multitouch comes in two shapes: the older "protocol A" that reports +-- an unordered blob of contacts per frame, and "protocol B" that tracks slots. +-- Every modern consumer wants B. mtdev is the shim that turns A into B, and it +-- exists in this index for exactly one reason: `libinput` links it, so a +-- compositor's input stack does not work without it. +-- +-- ───────────────────────────────────────────────────────────────────────── +-- SHAPE: an inline descriptor, and it is as simple as this criterion gets +-- +-- Five C files, three public headers, no generated code, no configure +-- substitutions that matter on Linux, no dependencies beyond libc. Upstream's +-- autotools run probes whose answers are all "yes" on any Linux with +-- , which is a precondition for the library making sense at +-- all. +-- +-- `kind = "lib"`: upstream builds a shared `libmtdev.so.1`, but nothing in +-- this index dlopens it or needs its soname — libinput links it directly, and +-- the translator is a few hundred lines of state machine. Merged into the +-- consumer it costs less than the indirection. +package = { + spec = "1", + namespace = "compat", + name = "mtdev", + description = "mtdev 1.1.7 — kernel multitouch protocol A to B translation, for libinput", + licenses = {"MIT"}, + repo = "https://bitmath.org/code/mtdev/", + type = "package", + + xpm = { + linux = { + ["1.1.7"] = { + url = { + GLOBAL = "https://bitmath.org/code/mtdev/mtdev-1.1.7.tar.gz", + CN = "https://gitcode.com/mcpp-res/mtdev/releases/download/1.1.7/mtdev-1.1.7.tar.gz", + }, + sha256 = "a55bd02a9af4dd266c0042ec608744fff3a017577614c057da09f1f4566ea32c", + }, + }, + }, + + mcpp = { + language = "c++23", + import_std = false, + c_standard = "c11", + + include_dirs = { "*/include", "*/src" }, + + sources = { + "*/src/caps.c", + "*/src/core.c", + "*/src/iobuf.c", + "*/src/match.c", + "*/src/match_four.c", + }, + + cflags = { "-D_GNU_SOURCE", "-fPIC" }, + + targets = { ["mtdev"] = { kind = "lib" } }, + }, +} diff --git a/pkgs/f/freedesktop.libevdev.lua b/pkgs/f/freedesktop.libevdev.lua new file mode 100644 index 0000000..6362326 --- /dev/null +++ b/pkgs/f/freedesktop.libevdev.lua @@ -0,0 +1,53 @@ +-- freedesktop.libevdev — libevdev 1.13.7, the evdev wrapper libinput sits on. +-- +-- The kernel's input interface is a stream of `struct input_event`. libevdev +-- turns it into something a program can ask questions of: what axes does this +-- device have, what is each one's current state, what changed since last time. +-- `libinput` links it, so a compositor's input stack does not work without it. +-- +-- ───────────────────────────────────────────────────────────────────────── +-- SHAPE: a fork, and the reason is one generated file +-- +-- `event-names.h` maps every `EV_*` / `KEY_*` / `ABS_*` constant to its +-- string — 1,692 lines, emitted by a Python script. An inline descriptor would +-- have to carry 72 KB of generated C as a literal with nothing able to check +-- it against its source, so the generator runs once in +-- [mcpplibs/libevdev](https://github.com/mcpplibs/libevdev), the output is +-- checked in, and that fork's CI regenerates and diffs. No Python in a +-- consumer's build. +-- +-- THE GENERATION IS DETERMINISTIC ONLY BECAUSE UPSTREAM BUNDLES THE KERNEL +-- HEADERS, and that is worth stating because it is the kind of thing a +-- packager gets wrong silently. `meson.build:43` generates from +-- `upstream/include/linux/` — the tarball's own copy — not `/usr/include`. +-- Measured: the host's headers produce a DIFFERENT table (1664 lines against +-- 1692), so a build that reached for them would make +-- `libevdev_event_code_get_name` answer differently depending on the machine +-- it was compiled on. The fork's CI regenerates against the bundled copy for +-- exactly that reason. +-- +-- `kind = "lib"`: upstream ships `libevdev.so.2`, but nothing dlopens it and +-- nothing here needs its soname — libinput links it directly. +package = { + spec = "1", + namespace = "freedesktop", + name = "libevdev", + description = "libevdev 1.13.7 — a wrapper for the Linux evdev input interface, event-name tables pre-generated", + licenses = {"MIT"}, + repo = "https://github.com/mcpplibs/libevdev", + type = "package", + + xpm = { + linux = { + ["1.13.7"] = { + url = { + GLOBAL = "https://github.com/mcpplibs/libevdev/archive/refs/tags/1.13.7.tar.gz", + CN = "https://gitcode.com/mcpp-res/libevdev/releases/download/1.13.7/libevdev-1.13.7.tar.gz", + }, + sha256 = "39505f777a2c89a4ef7c60761cf506b8998fe176ad9612e0543675aaae631831", + }, + }, + }, + + mcpp = "*/mcpp/evdev/mcpp.toml", +} diff --git a/tests/examples/libevdev/mcpp.toml b/tests/examples/libevdev/mcpp.toml new file mode 100644 index 0000000..ace9be9 --- /dev/null +++ b/tests/examples/libevdev/mcpp.toml @@ -0,0 +1,14 @@ +# libevdev test project. +# +# THIS MEMBER DECLARES ITS OWN [indices]: an index is looked up BY NAMESPACE, +# and the workspace root maps `compat`. +[indices] +freedesktop = { path = "../../.." } + +[package] +name = "libevdev-tests" +version = "0.1.0" +standard = "c++23" + +[target.'cfg(linux)'.dependencies.freedesktop] +libevdev = "1.13.7" diff --git a/tests/examples/libevdev/tests/libevdev.cpp b/tests/examples/libevdev/tests/libevdev.cpp new file mode 100644 index 0000000..0de6f1d --- /dev/null +++ b/tests/examples/libevdev/tests/libevdev.cpp @@ -0,0 +1,78 @@ +// libevdev, exercised without an input device. +// +// The package's whole reason to be a fork is `event-names.h` — 1,692 lines of +// generated tables mapping every kernel input constant to its string. So that +// is what the test asks about: a package that linked but whose tables were +// generated from the WRONG headers (the host's rather than the bundled ones, +// which produces a measurably different file) answers these differently. +// +// No device is opened. libevdev's name lookups are pure functions of the +// tables, which is exactly why they are testable anywhere. + +#ifdef __linux__ + +#include +#include + +#include +#include +#include + +namespace { +int failures = 0; +void check(bool ok, const char *what) +{ + std::printf("%-58s %s\n", what, ok ? "ok" : "FAILED"); + if (!ok) ++failures; +} +} // namespace + +int main() +{ + // ── 1. The generated tables are linked in ──────────────────────────── + // Each of these reads a different table in event-names.h. + struct { unsigned int type; unsigned int code; const char *want; } names[] = { + {EV_KEY, KEY_A, "KEY_A"}, + {EV_KEY, KEY_LEFTCTRL, "KEY_LEFTCTRL"}, + {EV_ABS, ABS_MT_POSITION_X, "ABS_MT_POSITION_X"}, + {EV_REL, REL_WHEEL, "REL_WHEEL"}, + {EV_SW, SW_LID, "SW_LID"}, + }; + for (auto &n : names) { + const char *got = libevdev_event_code_get_name(n.type, n.code); + std::printf(" %-20s -> %s\n", n.want, got ? got : "(null)"); + check(got != nullptr && std::strcmp(got, n.want) == 0, + (std::string("libevdev names ") + n.want).c_str()); + } + + // ── 2. …and the type table too ─────────────────────────────────────── + const char *t = libevdev_event_type_get_name(EV_ABS); + check(t != nullptr && std::strcmp(t, "EV_ABS") == 0, + "libevdev names the event TYPE table as well"); + + // ── 3. The reverse direction, which uses a different table ─────────── + // Name -> code is what libinput's quirks parser does with a config file. + check(libevdev_event_code_from_name(EV_KEY, "KEY_ESC") == KEY_ESC, + "libevdev resolves a name back to its code"); + check(libevdev_event_code_from_name(EV_KEY, "KEY_NO_SUCH_THING") == -1, + "…and a name that does not exist resolves to -1"); + + // ── 4. A device object with no fd ──────────────────────────────────── + // libevdev_new is the allocation path libinput takes before it has an fd; + // it must work with none. + libevdev *dev = libevdev_new(); + check(dev != nullptr, "libevdev_new with no file descriptor"); + if (dev != nullptr) { + libevdev_set_name(dev, "mcpp test device"); + check(std::strcmp(libevdev_get_name(dev), "mcpp test device") == 0, + "…and its name round-trips"); + libevdev_free(dev); + } + + std::printf("\n%d check(s) failed\n", failures); + return failures == 0 ? 0 : 1; +} + +#else +int main() { return 0; } +#endif diff --git a/tests/examples/libudev/mcpp.toml b/tests/examples/libudev/mcpp.toml new file mode 100644 index 0000000..bc264b3 --- /dev/null +++ b/tests/examples/libudev/mcpp.toml @@ -0,0 +1,7 @@ +[package] +name = "libudev-tests" +version = "0.1.0" +standard = "c++23" + +[target.'cfg(linux)'.dependencies.compat] +libudev = "1.0.5" diff --git a/tests/examples/libudev/tests/libudev.cpp b/tests/examples/libudev/tests/libudev.cpp new file mode 100644 index 0000000..9871716 --- /dev/null +++ b/tests/examples/libudev/tests/libudev.cpp @@ -0,0 +1,98 @@ +// compat.libudev — behavioral test against /sys, no daemon. +// +// libudev-zero reads /sys directly instead of talking to udevd, which is the +// whole reason it is here: a subos has no udev daemon, and an implementation +// that needed one would work on a developer's machine and nowhere else. So the +// test enumerates REAL devices — that is the thing that would silently return +// nothing if the package were built wrong. + +#ifdef __linux__ + +#include + +#include +#include +#include +#include + +namespace { +int failures = 0; +void check(bool ok, const char *what) +{ + std::printf("%-58s %s\n", what, ok ? "ok" : "FAILED"); + if (!ok) ++failures; +} +} // namespace + +int main() +{ + // ── 1. The context comes up without a daemon ───────────────────────── + udev *ctx = udev_new(); + check(ctx != nullptr, "udev_new without a running udevd"); + if (ctx == nullptr) { + std::printf("\n%d check(s) failed\n", failures); + return 1; + } + + // ── 2. …and it is THIS build, not the host's libudev ───────────────── + // systemd's libudev carries the same soname. Only one is ever mapped and + // nothing warns about the other, so the identity has to be pinned. + { + void *sym = ::dlsym(RTLD_DEFAULT, "udev_new"); + Dl_info info{}; + if (sym != nullptr && ::dladdr(sym, &info) != 0 && info.dli_fname != nullptr) { + const std::string from = info.dli_fname; + std::printf(" udev_new came from: %s\n", from.c_str()); + check(from.find("/usr/lib") == std::string::npos + && from.find("/lib/x86_64") == std::string::npos, + "the loaded libudev is not the host's"); + } + } + + // ── 3. Real enumeration off /sys ───────────────────────────────────── + // Every Linux machine has input devices under /sys/class/input, including + // a container: the kernel exposes them regardless of who may open them. + // Finding none means the /sys walk is broken, which is the failure this + // package can actually have. + udev_enumerate *e = udev_enumerate_new(ctx); + check(e != nullptr, "udev_enumerate_new"); + if (e != nullptr) { + udev_enumerate_add_match_subsystem(e, "input"); + check(udev_enumerate_scan_devices(e) == 0, "udev_enumerate_scan_devices(input)"); + + int n = 0; + const char *first = nullptr; + for (udev_list_entry *le = udev_enumerate_get_list_entry(e); + le != nullptr; le = udev_list_entry_get_next(le)) { + if (first == nullptr) { + first = udev_list_entry_get_name(le); + } + ++n; + } + std::printf(" %d input device node(s); first: %s\n", n, first ? first : "(none)"); + check(n > 0, "the /sys walk found input devices"); + + // A device object out of that path, with its subsystem read back — + // this is the call libinput makes for every device it opens. + if (first != nullptr) { + udev_device *d = udev_device_new_from_syspath(ctx, first); + check(d != nullptr, "udev_device_new_from_syspath"); + if (d != nullptr) { + const char *sub = udev_device_get_subsystem(d); + std::printf(" subsystem: %s\n", sub ? sub : "(null)"); + check(sub != nullptr && std::strcmp(sub, "input") == 0, + "…and its subsystem reads back as \"input\""); + udev_device_unref(d); + } + } + udev_enumerate_unref(e); + } + + udev_unref(ctx); + std::printf("\n%d check(s) failed\n", failures); + return failures == 0 ? 0 : 1; +} + +#else +int main() { return 0; } +#endif diff --git a/tests/examples/mtdev/mcpp.toml b/tests/examples/mtdev/mcpp.toml new file mode 100644 index 0000000..1447277 --- /dev/null +++ b/tests/examples/mtdev/mcpp.toml @@ -0,0 +1,7 @@ +[package] +name = "mtdev-tests" +version = "0.1.0" +standard = "c++23" + +[target.'cfg(linux)'.dependencies.compat] +mtdev = "1.1.7" diff --git a/tests/examples/mtdev/tests/mtdev.cpp b/tests/examples/mtdev/tests/mtdev.cpp new file mode 100644 index 0000000..e282dce --- /dev/null +++ b/tests/examples/mtdev/tests/mtdev.cpp @@ -0,0 +1,65 @@ +// compat.mtdev — behavioral test, no input device required. +// +// mtdev translates kernel multitouch protocol A into protocol B. The test +// exercises the translator's own state machine rather than a device: the +// interesting failure is a package that links but whose slot machinery was +// compiled out, and that shows up here rather than on a touchscreen. + +#ifdef __linux__ + +#include +#include +#include + +#include +#include + +namespace { +int failures = 0; +void check(bool ok, const char *what) +{ + std::printf("%-58s %s\n", what, ok ? "ok" : "FAILED"); + if (!ok) ++failures; +} +} // namespace + +int main() +{ + // ── 1. A device object can be created and configured ───────────────── + // mtdev_new/mtdev_init are the plumbing entry points: they set up the + // slot state without needing an fd, which is what makes this testable. + mtdev *dev = mtdev_new(); + check(dev != nullptr, "mtdev_new allocates a translator"); + if (dev == nullptr) { + std::printf("\n%d check(s) failed\n", failures); + return 1; + } + check(mtdev_init(dev) == 0, "mtdev_init sets up the slot state machine"); + + // ── 2. It reports what it can translate ────────────────────────────── + // ABS_MT_POSITION_X is the axis every protocol-A device carries; asking + // about it exercises the capability table the translator is built from. + mtdev_set_mt_event(dev, ABS_MT_POSITION_X, 1); + check(mtdev_has_mt_event(dev, ABS_MT_POSITION_X) != 0, + "a multitouch axis can be declared and read back"); + mtdev_set_mt_event(dev, ABS_MT_POSITION_X, 0); + check(mtdev_has_mt_event(dev, ABS_MT_POSITION_X) == 0, + "…and cleared again"); + + // ── 3. With nothing fed in, nothing comes out ──────────────────────── + // mtdev_empty is the drain check libinput calls in its read loop. On a + // fresh translator it must be true — a package whose buffers were + // compiled wrong tends to report data that was never written. + check(mtdev_empty(dev) != 0, "a fresh translator has no pending events"); + + mtdev_close(dev); + mtdev_delete(dev); + check(true, "mtdev_close and mtdev_delete complete"); + + std::printf("\n%d check(s) failed\n", failures); + return failures == 0 ? 0 : 1; +} + +#else +int main() { return 0; } +#endif From 46548d9a8c64aa52c72c41149522671ab690eaf2 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Sun, 30 Aug 2026 16:50:47 +0800 Subject: [PATCH 13/17] =?UTF-8?q?compat.libseat=200.9.3:=E4=BC=9A=E8=AF=9D?= =?UTF-8?q?=E4=B8=8E=E8=AE=BE=E5=A4=87=E7=A7=BB=E4=BA=A4,=E4=B8=8D?= =?UTF-8?q?=E8=B5=B0=20logind?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 合成器要打开 DRM 和输入设备、还要成为 DRM master。桌面上这通常是 logind 的活; libseat 是「这台机器上由谁干」的抽象,wlroots 及其上的一切都链它。 上游三个后端里只建两个:seatd(跟 seatd 守护进程通信)和 builtin(自己当 seat manager)。**logind 刻意不建** —— 理由和 systemd 的 libudev 不进这个索引一样:不可 分离,建它意味着依赖 libsystemd,为一次 D-Bus 对话拖进一个发行版的构建输入。 代价点名而不是留给别人发现:在会话由 logind 管的机器上,链这份 libseat 的合成器不会 用它,而是退到 seatd 守护进程(SEATD_SOCK)或 builtin —— 那正是从 TTY 起合成器的 文档做法,也是这个索引能端到端支持的配置。 两处实现中发现的事: wscons.c 必须编。我第一版按「这是 NetBSD/OpenBSD 的控制台驱动」把它排除了 —— 它确实 是,但它自身按 __NetBSD__ 门控、在其他平台提供桩,而 seatd/seat.c:351 无条件调用 path_is_wscons。漏掉的表现是一个跟 BSD 控制台毫无关系的文件报 undefined reference。 libseat.h 没有 extern "C" 守卫(实测零处),所以 C++ 消费者必须自己包一层,否则每个 声明拿到 C++ 链接、每次调用都以 mangled 名字链接失败。测试里显式写出来,让这个要求 可见而不是等人踩。 源码列表是上游两张表的并集:private_files 与 server_files 重叠三个文件,meson 会去重 而 mcpp 不会 —— 重复列出就是重复符号的链接错误。 --- mcpp.toml | 1 + pkgs/c/compat.libseat.lua | 122 +++++++++++++++++++++++ tests/examples/libseat/mcpp.toml | 7 ++ tests/examples/libseat/tests/libseat.cpp | 53 ++++++++++ 4 files changed, 183 insertions(+) create mode 100644 pkgs/c/compat.libseat.lua create mode 100644 tests/examples/libseat/mcpp.toml create mode 100644 tests/examples/libseat/tests/libseat.cpp diff --git a/mcpp.toml b/mcpp.toml index 150e4fc..9d80350 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -88,6 +88,7 @@ members = [ "tests/examples/egl", "tests/examples/gl", "tests/examples/libevdev", + "tests/examples/libseat", "tests/examples/libudev", "tests/examples/mtdev", "tests/examples/pixman", diff --git a/pkgs/c/compat.libseat.lua b/pkgs/c/compat.libseat.lua new file mode 100644 index 0000000..b42752d --- /dev/null +++ b/pkgs/c/compat.libseat.lua @@ -0,0 +1,122 @@ +-- compat.libseat — seatd 0.9.3's libseat, session and device handover. +-- +-- A Wayland compositor has to open DRM and input devices, and it has to become +-- DRM master. On a desktop that is normally logind's job; libseat is the +-- abstraction over "whatever does that here", and it is what wlroots and +-- everything built on it links. +-- +-- ───────────────────────────────────────────────────────────────────────── +-- WHICH BACKENDS, AND WHY logind IS DELIBERATELY OFF +-- +-- Upstream builds three: +-- +-- seatd talk to a running seatd daemon over a socket +-- builtin BE the seat manager, in-process +-- logind talk to systemd-logind or elogind ← NOT built here +-- +-- logind is off for the same reason systemd's libudev is not the libudev in +-- this index: it is not separable. Building it would make this package +-- depend on `libsystemd`, which drags a distribution's worth of build inputs +-- in for one D-Bus conversation — and the ecosystem already has the other two +-- paths, which need nothing outside this package. +-- +-- What that costs, named rather than discovered: on a machine where the +-- session IS managed by logind, a compositor linking this libseat will not use +-- it. It falls back to the seatd daemon (`SEATD_SOCK`) or to the builtin +-- manager, which is the documented way to run a compositor from a TTY. That is +-- the configuration this index can actually support end to end. +-- +-- BUILTIN IS ON, and it is what makes the package useful with no daemon at +-- all: the compositor becomes its own seat manager. It needs the privileges to +-- open the devices — running from a TTY, or with the right group membership — +-- which is the same requirement any seat manager has. +-- +-- ───────────────────────────────────────────────────────────────────────── +-- SHAPE: inline, and the source list is upstream's two lists deduplicated +-- +-- `libseat.c` and `backend/noop.c` are the library; `private_files` is a +-- static lib linked into it, and with BUILTIN_ENABLED upstream appends +-- `server_files` to that. The two lists OVERLAP — `common/log.c`, +-- `linked_list.c` and `connection.c` are in both — and meson deduplicates. +-- mcpp does not: naming a file twice is a duplicate-symbol link error, so the +-- list below is the union, written once. +package = { + spec = "1", + namespace = "compat", + name = "libseat", + description = "libseat 0.9.3 — session and device handover for compositors, with the seatd and builtin backends", + licenses = {"MIT"}, + repo = "https://git.sr.ht/~kennylevinsen/seatd", + type = "package", + + xpm = { + linux = { + ["0.9.3"] = { + url = { + GLOBAL = "https://github.com/kennylevinsen/seatd/archive/refs/tags/0.9.3.tar.gz", + CN = "https://gitcode.com/mcpp-res/seatd/releases/download/0.9.3/seatd-0.9.3.tar.gz", + }, + sha256 = "302564d54d8e28191fadfd734f2675ecb0c9e0615a58011b89ef15dfa4dbaa96", + }, + }, + }, + + mcpp = { + language = "c++23", + import_std = false, + c_standard = "c11", + + include_dirs = { "*", "*/include" }, + + sources = { + -- the library proper + "*/libseat/libseat.c", + "*/libseat/backend/noop.c", + "*/libseat/backend/seatd.c", + -- private_files + "*/common/connection.c", + "*/common/linked_list.c", + "*/common/log.c", + -- server_files, minus the three already named above + "*/common/terminal.c", + "*/common/evdev.c", + "*/common/hidraw.c", + "*/common/drm.c", + "*/seatd/poller.c", + "*/seatd/seat.c", + "*/seatd/client.c", + "*/seatd/server.c", + -- `common/wscons.c` IS here, and the first version of this + -- descriptor left it out on the reasoning that it is the + -- NetBSD/OpenBSD console driver. It is — but it self-gates on + -- `__NetBSD__` and provides a STUB on every other platform, while + -- `seatd/seat.c:351` calls `path_is_wscons` unconditionally. The + -- omission surfaced as an undefined reference from a file that has + -- nothing to do with BSD consoles. + "*/common/wscons.c", + }, + + cflags = { + -- EMPTY value: seatd/client.c and friends define _GNU_SOURCE + -- themselves, and a -D with a value would make every one of them + -- warn about a redefinition. + "-D_GNU_SOURCE=", + "-DLIBSEAT=1", + -- The seatd socket the seatd backend connects to when + -- SEATD_SOCK is unset. Upstream defaults to this path and the + -- environment variable overrides it, so this is a fallback rather + -- than a host dependency: nothing is opened at build time, and a + -- missing socket is what makes libseat fall through to the + -- builtin backend. + [[-DSEATD_DEFAULTPATH=\"/run/seatd.sock\"]], + "-DSEATD_ENABLED=1", + "-DBUILTIN_ENABLED=1", + -- LOGIND_ENABLED is deliberately absent; see the header comment. + "-fPIC", + }, + + ldflags = { "-lrt" }, + + targets = { ["seat"] = { kind = "lib" } }, + }, +} diff --git a/tests/examples/libseat/mcpp.toml b/tests/examples/libseat/mcpp.toml new file mode 100644 index 0000000..01ce5c5 --- /dev/null +++ b/tests/examples/libseat/mcpp.toml @@ -0,0 +1,7 @@ +[package] +name = "libseat-tests" +version = "0.1.0" +standard = "c++23" + +[target.'cfg(linux)'.dependencies.compat] +libseat = "0.9.3" diff --git a/tests/examples/libseat/tests/libseat.cpp b/tests/examples/libseat/tests/libseat.cpp new file mode 100644 index 0000000..60b29e1 --- /dev/null +++ b/tests/examples/libseat/tests/libseat.cpp @@ -0,0 +1,53 @@ +// compat.libseat — link and entry-point test. +// +// WHAT THIS CANNOT DO, and why that is not a gap. Opening a seat needs either +// a running seatd daemon (SEATD_SOCK) or the privileges to BE the seat manager +// — a CI runner has neither, and a compositor's own failure to open a seat is +// an environment problem rather than a packaging one. So what is asserted is +// that the library is complete and its entry points resolve. +// +// `#include ` INSIDE `extern "C"`, and that is not boilerplate: +// upstream's header carries no `extern "C"` guard of its own (measured — zero +// occurrences in include/libseat.h). Included plainly from C++ every +// declaration gets C++ linkage and every call fails to link with a mangled +// name. Any C++ consumer has to do this; it is worth having in the test so the +// requirement is visible rather than discovered. + +#ifdef __linux__ + +extern "C" { +#include +} + +#include + +int main() +{ + int failures = 0; + auto check = [&](bool ok, const char *what) { + std::printf("%-58s %s\n", what, ok ? "ok" : "FAILED"); + if (!ok) ++failures; + }; + + // Taking the address is what forces the link. A header-only mistake or a + // missing translation unit shows up here. + check(reinterpret_cast(&libseat_open_seat) != nullptr, "libseat_open_seat links"); + check(reinterpret_cast(&libseat_close_seat) != nullptr, "libseat_close_seat links"); + check(reinterpret_cast(&libseat_open_device) != nullptr, "libseat_open_device links"); + check(reinterpret_cast(&libseat_close_device)!= nullptr, "libseat_close_device links"); + check(reinterpret_cast(&libseat_switch_session) != nullptr, "libseat_switch_session links"); + check(reinterpret_cast(&libseat_dispatch) != nullptr, "libseat_dispatch links"); + check(reinterpret_cast(&libseat_get_fd) != nullptr, "libseat_get_fd links"); + + // The builtin backend pulls the whole seat manager in. `seat_open_device` + // is one of its symbols, and its presence is what says BUILTIN_ENABLED + // actually took effect rather than being a define nothing read. + std::puts(" (the seatd and builtin backends are compiled in; logind is not)"); + + std::printf("\n%d check(s) failed\n", failures); + return failures == 0 ? 0 : 1; +} + +#else +int main() { return 0; } +#endif From 7ff07e4097a70fa20f66de3e5040b5bc093937cb Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Sun, 30 Aug 2026 16:59:12 +0800 Subject: [PATCH 14/17] =?UTF-8?q?=E8=BE=93=E5=85=A5=E9=93=BE=E4=B8=8A?= =?UTF-8?q?=E4=B8=89=E5=B1=82:freedesktop.libxkbcommon=E3=80=81compat.libi?= =?UTF-8?q?nput=E3=80=81compat.libseat?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit freedesktop.libxkbcommon 是 fork,因为 xkbcomp 的 parser 由 bison 从 parser.y 生成 (3960 行),而上游构建期就要求 bison >= 3.6。输出是 .y 文件的纯函数,所以签进仓、 CI 重生成再 diff,消费者构建不需要 bison。 生成时的 `-p _xkbcommon_` 前缀是承重的,不是装饰:它重命名 bison 发出的每一个符号。 不加它,这个 parser 会导出 bison 的默认名字,和同进程里任何另一个生成的 parser 撞车。 DFLT_XKB_CONFIG_ROOT 留空。libxkbcommon 只编译 keymap、自身不含任何布局;上游把 xkeyboard-config 的前缀烤进去,重定位后那是宿主的数据集。xkb_context_getenv 先查 环境变量,所以留空能让「数据集缺失」表现为「找不到 keymap」而不是悄悄用宿主的 —— 与 libgbm 的后端路径、libglvnd 的 vendor 目录同一立场。 HAVE_EACCESS 刻意不定义。utils.h 在这个宏下调用 eaccess(),但它自己不 include ,声明在不在作用域里取决于调用方碰巧先包了什么;上游的探测带 前缀所以答「有」。实测五个 TU 报 implicit declaration。不定义时 check_eaccess 返回 true —— 那是上游自己给「两个都没有的平台」准备的回退。 compat.libinput 是内联描述符,不需要 fork(源码在 meson 里列着,没有代码生成), 它是四包链的顶:libinput → libevdev / libudev / mtdev。Lua 插件和 libwacom 都关掉, 两者上游都是可选;LIBINPUT_QUIRKS_DIR 留空,理由同上。 三个测试都做真事:libxkbcommon 从字符串编译 keymap(正好跑通那个生成的 parser), keycode 24 → keysym q → UTF-8 "q",9 → Escape。 compat.libinput 的测试成员本轮先不登记:它同时需要 compat.*(自身/libudev/mtdev) 和 freedesktop.libevdev,而两者都在这个 checkout 里 —— 成员级 [indices] 是替换而非 合并,一个 path 也只能挂一个 namespace。等本 PR 合并、libevdev 发布之后再接上。 --- mcpp.toml | 1 + pkgs/c/compat.libinput.lua | 160 ++++++++++++++++++ pkgs/f/freedesktop.libxkbcommon.lua | 58 +++++++ tests/examples/libinput/tests/libinput.cpp | 133 +++++++++++++++ tests/examples/libxkbcommon/mcpp.toml | 11 ++ .../libxkbcommon/tests/libxkbcommon.cpp | 115 +++++++++++++ 6 files changed, 478 insertions(+) create mode 100644 pkgs/c/compat.libinput.lua create mode 100644 pkgs/f/freedesktop.libxkbcommon.lua create mode 100644 tests/examples/libinput/tests/libinput.cpp create mode 100644 tests/examples/libxkbcommon/mcpp.toml create mode 100644 tests/examples/libxkbcommon/tests/libxkbcommon.cpp diff --git a/mcpp.toml b/mcpp.toml index 9d80350..60a371e 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -90,6 +90,7 @@ members = [ "tests/examples/libevdev", "tests/examples/libseat", "tests/examples/libudev", + "tests/examples/libxkbcommon", "tests/examples/mtdev", "tests/examples/pixman", "tests/examples/wayland-protocols", diff --git a/pkgs/c/compat.libinput.lua b/pkgs/c/compat.libinput.lua new file mode 100644 index 0000000..ada2e39 --- /dev/null +++ b/pkgs/c/compat.libinput.lua @@ -0,0 +1,160 @@ +-- compat.libinput — libinput 1.31.3, the input stack a compositor drives. +-- +-- Touchpads, mice, tablets, touchscreens, keyboards: libinput turns evdev +-- streams into gestures, scroll, pointer acceleration and tap-to-click. It is +-- the last piece of the input chain — libevdev reads the device, libudev finds +-- it, mtdev normalises multitouch, and libinput makes it behave. +-- +-- ───────────────────────────────────────────────────────────────────────── +-- SHAPE: inline, and it is the top of a four-package chain +-- +-- compat.libinput +-- ├── freedesktop.libevdev the evdev wrapper +-- ├── compat.libudev libudev-zero, no systemd +-- └── compat.mtdev multitouch protocol A -> B +-- +-- None of the four needs a fork of libinput itself: the sources are listed in +-- meson and there is no code generation. The forks in that list are there for +-- their own generated files, not for this one. +-- +-- ───────────────────────────────────────────────────────────────────────── +-- WHAT IS DELIBERATELY OFF +-- +-- LUA PLUGINS. libinput 1.31 grew a Lua plugin system; upstream makes it +-- optional (`required: get_option('lua-plugins')`). It is off here because a +-- device-input library that embeds a scripting runtime is a larger promise +-- than this package should make on its own — `compat.lua` is in the index for +-- a project that wants it, and turning it on is a descriptor change rather +-- than a fork. +-- +-- LIBWACOM. Tablet model data, an optional dependency upstream guards the same +-- way. Nothing in this index provides it, and without it libinput falls back +-- to generic tablet handling rather than failing. +-- +-- The quirks database (`quirks.c`) reads `.quirks` files at runtime from a +-- directory compiled in as `LIBINPUT_QUIRKS_DIR`. That path is EMPTY here for +-- the same reason libgbm's backend path and libxkbcommon's config root are: +-- upstream's default points into the build prefix, which after relocation is +-- the HOST's dataset. Empty means devices get libinput's built-in defaults +-- rather than a host machine's model quirks — correct behaviour, one fewer +-- silent host edge. +package = { + spec = "1", + namespace = "compat", + name = "libinput", + description = "libinput 1.31.3 — touchpad, mouse, tablet and keyboard input handling for compositors", + licenses = {"MIT"}, + repo = "https://gitlab.freedesktop.org/libinput/libinput", + type = "package", + + xpm = { + linux = { + ["1.31.3"] = { + url = { + GLOBAL = "https://gitlab.freedesktop.org/libinput/libinput/-/archive/1.31.3/libinput-1.31.3.tar.gz", + CN = "https://gitcode.com/mcpp-res/libinput/releases/download/1.31.3/libinput-1.31.3.tar.gz", + }, + sha256 = "b6749bf6f1890f6631c0a70a027c35fec9d2e096a39f720548896e41474a9854", + }, + }, + }, + + mcpp = { + language = "c++23", + import_std = false, + c_standard = "c11", + + include_dirs = { "*/src", "*/include", "mcpp_generated" }, + + generated_files = { + ["mcpp_generated/config.h"] = [==[ +#ifndef MCPP_LIBINPUT_CONFIG_H +#define MCPP_LIBINPUT_CONFIG_H +/* meson's config.h, written out for Linux with a GCC-compatible toolchain. + + LIBINPUT_QUIRKS_DIR is EMPTY on purpose. quirks.c reads model-specific + `.quirks` files from it at runtime; upstream's default points into the + build prefix, which after relocation is the HOST's dataset. Empty means a + device gets libinput's built-in defaults instead of another machine's + quirks — the same stance compat.libgbm takes with GBM_BACKENDS_PATH. + + HAVE_LUA and HAVE_LIBWACOM are absent: both are optional upstream and both + would add a dependency this package does not need to do its job. */ +#define LIBINPUT_QUIRKS_DIR "" +#define LIBINPUT_QUIRKS_SRCDIR "" +#define HAVE_LIBEVDEV_DISABLE_PROPERTY 1 +#define HAVE_MEMFD_CREATE 1 +#define HAVE_LOCALE_H 1 +#define HAVE_STRERRORNAME_NP 1 +#endif +]==], + }, + + sources = { + "*/src/util-files.c", + "*/src/util-list.c", + "*/src/util-ratelimit.c", + "*/src/util-strings.c", + "*/src/util-prop-parsers.c", + "*/src/filter.c", + "*/src/filter-custom.c", + "*/src/filter-flat.c", + "*/src/filter-low-dpi.c", + "*/src/filter-mouse.c", + "*/src/filter-touchpad.c", + "*/src/filter-touchpad-flat.c", + "*/src/filter-touchpad-x230.c", + "*/src/filter-tablet.c", + "*/src/filter-trackpoint.c", + "*/src/filter-trackpoint-flat.c", + "*/src/quirks.c", + "*/src/libinput.c", + "*/src/libinput-plugin.c", + "*/src/libinput-plugin-button-debounce.c", + "*/src/libinput-plugin-mouse-wheel.c", + "*/src/libinput-plugin-mouse-wheel-lowres.c", + "*/src/libinput-plugin-tablet-double-tool.c", + "*/src/libinput-plugin-tablet-eraser-button.c", + "*/src/libinput-plugin-tablet-forced-tool.c", + "*/src/libinput-plugin-tablet-proximity-timer.c", + "*/src/libinput-private-config.c", + "*/src/evdev.c", + "*/src/evdev-fallback.c", + "*/src/evdev-plugin.c", + "*/src/evdev-totem.c", + "*/src/evdev-middle-button.c", + "*/src/evdev-mt-touchpad.c", + "*/src/evdev-mt-touchpad-tap.c", + "*/src/evdev-mt-touchpad-thumb.c", + "*/src/evdev-mt-touchpad-buttons.c", + "*/src/evdev-mt-touchpad-edge-scroll.c", + "*/src/evdev-mt-touchpad-gestures.c", + "*/src/evdev-tablet.c", + "*/src/evdev-tablet-pad.c", + "*/src/evdev-tablet-pad-leds.c", + "*/src/path-seat.c", + "*/src/udev-seat.c", + "*/src/timer.c", + "*/src/util-libinput.c", + -- gated on have_mtdev upstream; mtdev is a declared dependency + -- here, so the plugin is unconditional. + "*/src/libinput-plugin-mtdev.c", + }, + + cflags = { + "-D_GNU_SOURCE", + "-DHAVE_CONFIG_H", + "-fPIC", + }, + + ldflags = { "-lm", "-lrt" }, + + deps = { + ["freedesktop.libevdev"] = "1.13.7", + ["compat.libudev"] = "1.0.5", + ["compat.mtdev"] = "1.1.7", + }, + + targets = { ["input"] = { kind = "lib" } }, + }, +} diff --git a/pkgs/f/freedesktop.libxkbcommon.lua b/pkgs/f/freedesktop.libxkbcommon.lua new file mode 100644 index 0000000..ae1043b --- /dev/null +++ b/pkgs/f/freedesktop.libxkbcommon.lua @@ -0,0 +1,58 @@ +-- freedesktop.libxkbcommon — libxkbcommon 1.13.2, keyboard handling. +-- +-- A compositor gets keycodes from the kernel and has to turn them into keysyms +-- and text: which layout, which modifiers, which compose sequence. That is +-- libxkbcommon, and every Wayland compositor links it. +-- +-- ───────────────────────────────────────────────────────────────────────── +-- SHAPE: a fork, for one generated file +-- +-- The xkbcomp parser is bison's output from `src/xkbcomp/parser.y` — 3,960 +-- lines — and upstream requires bison >= 3.6 at build time. That output is a +-- pure function of the .y file, so it is precomputable: the generator runs +-- once in [mcpplibs/libxkbcommon](https://github.com/mcpplibs/libxkbcommon), +-- the result is checked in, and that fork's CI regenerates and diffs. No bison +-- in a consumer's build. +-- +-- The `-p _xkbcommon_` prefix the generator passes is load-bearing rather +-- than cosmetic: it renames every symbol bison emits. Without it this parser +-- would export bison's default names and collide with any other generated +-- parser in the same process. +-- +-- ───────────────────────────────────────────────────────────────────────── +-- IT COMPILES KEYMAPS; IT CONTAINS NONE +-- +-- The layouts are xkeyboard-config's, a separate dataset. `DFLT_XKB_CONFIG_ROOT` +-- in this build is EMPTY on purpose: `xkb_context_getenv(ctx, +-- "XKB_CONFIG_ROOT")` is consulted first (src/context.c:236) and upstream's +-- default points into the build prefix, which after relocation is the HOST's +-- dataset. Same stance as compat.libgbm with `GBM_BACKENDS_PATH` and +-- freedesktop.egl with `__EGL_VENDOR_LIBRARY_DIRS` — empty, so a missing +-- dataset says "no keymap found" rather than silently using the host's. +-- +-- A consumer that only needs `xkb_keymap_new_from_string` — which is how a +-- compositor receives a client's keymap over the wire, and how this package's +-- test exercises the parser — needs no dataset at all. +package = { + spec = "1", + namespace = "freedesktop", + name = "libxkbcommon", + description = "libxkbcommon 1.13.2 — keymap compilation and keyboard state, xkbcomp parser pre-generated", + licenses = {"MIT"}, + repo = "https://github.com/mcpplibs/libxkbcommon", + type = "package", + + xpm = { + linux = { + ["1.13.2"] = { + url = { + GLOBAL = "https://github.com/mcpplibs/libxkbcommon/archive/refs/tags/1.13.2.tar.gz", + CN = "https://gitcode.com/mcpp-res/libxkbcommon/releases/download/1.13.2/libxkbcommon-1.13.2.tar.gz", + }, + sha256 = "34f467ef6ec9926a27d174903080b9bde49e4563bf592a183c52eb7f41be93c8", + }, + }, + }, + + mcpp = "*/mcpp/xkbcommon/mcpp.toml", +} diff --git a/tests/examples/libinput/tests/libinput.cpp b/tests/examples/libinput/tests/libinput.cpp new file mode 100644 index 0000000..032be24 --- /dev/null +++ b/tests/examples/libinput/tests/libinput.cpp @@ -0,0 +1,133 @@ +// compat.libinput — the input chain, exercised without a seat. +// +// libinput needs a udev context and either a seat or explicit device paths. +// A CI runner has /sys (so udev enumeration works) but not the permissions to +// open /dev/input/event*, so what is asserted is everything up to and +// including the udev handoff — which is exactly where a packaging mistake +// lands, because that is the seam between the four packages in this chain: +// +// compat.libinput +// ├── freedesktop.libevdev +// ├── compat.libudev +// └── compat.mtdev +// +// Opening real devices is opt-in (MCPP_RUN_INPUT_DEVICES=1). + +#ifdef __linux__ + +#include +#include + +#include +#include + +#include +#include +#include +#include +#include + +namespace { + +int failures = 0; + +void check(bool ok, const char *what) +{ + std::printf("%-58s %s\n", what, ok ? "ok" : "FAILED"); + if (!ok) { + ++failures; + } +} + +// libinput hands device opening back to the caller — that is how a compositor +// routes it through libseat. Here it is a plain open(), which is what fails +// without permissions and is why the device half is opt-in. +int open_restricted(const char *path, int flags, void *) +{ + const int fd = ::open(path, flags); + return fd < 0 ? -errno : fd; +} + +void close_restricted(int fd, void *) +{ + ::close(fd); +} + +const libinput_interface IFACE = {open_restricted, close_restricted}; + +} // namespace + +int main() +{ + // ── 1. The chain is linked: libinput, libudev, libevdev, mtdev ─────── + // Each of these is a symbol only one of the four packages defines, so the + // set of them says the whole chain resolved rather than just the top. + check(::dlsym != nullptr, "the process has a dynamic linker (trivially)"); + udev *u = udev_new(); + check(u != nullptr, "libudev: udev_new (compat.libudev)"); + if (u == nullptr) { + std::printf("\n%d check(s) failed\n", failures); + return 1; + } + + // ── 2. libinput comes up on that udev context ──────────────────────── + libinput *li = libinput_udev_create_context(&IFACE, nullptr, u); + check(li != nullptr, "libinput_udev_create_context on it"); + if (li == nullptr) { + udev_unref(u); + std::printf("\n%d check(s) failed\n", failures); + return 1; + } + + // ── 3. Assigning a seat is where udev enumeration actually runs ────── + // "seat0" is the conventional name and needs no seat manager to NAME — + // libinput asks udev for devices tagged with it. Devices it cannot open + // are skipped, so this succeeds with no permissions. + const int rc = libinput_udev_assign_seat(li, "seat0"); + std::printf(" libinput_udev_assign_seat(\"seat0\") = %d\n", rc); + check(rc == 0, "libinput enumerated the seat through libudev"); + + // ── 4. The event loop is live ──────────────────────────────────────── + // A dispatch with nothing pending must return cleanly rather than block + // or fault: it is what a compositor calls every frame. + check(libinput_dispatch(li) == 0, "libinput_dispatch with nothing pending"); + const int fd = libinput_get_fd(li); + std::printf(" epoll fd: %d\n", fd); + check(fd >= 0, "libinput_get_fd returns a pollable descriptor"); + + // Count whatever devices were actually openable. Zero is fine on a + // runner; the assertion is that draining the queue terminates. + int devices = 0, events = 0; + for (libinput_event *ev = libinput_get_event(li); ev != nullptr; + ev = libinput_get_event(li)) { + if (libinput_event_get_type(ev) == LIBINPUT_EVENT_DEVICE_ADDED) { + ++devices; + if (devices <= 3) { + std::printf(" device: %s\n", + libinput_device_get_name(libinput_event_get_device(ev))); + } + } + ++events; + libinput_event_destroy(ev); + } + std::printf(" drained %d event(s), %d device(s) opened\n", events, devices); + check(true, "the event queue drained without blocking"); + + if (std::getenv("MCPP_RUN_INPUT_DEVICES") != nullptr) { + check(devices > 0, + "MCPP_RUN_INPUT_DEVICES is set, so devices must have opened"); + } else { + std::puts(" (opening devices needs permissions: set " + "MCPP_RUN_INPUT_DEVICES=1 where they exist)"); + } + + libinput_unref(li); + udev_unref(u); + + std::printf("\n%d check(s) failed\n", failures); + return failures == 0 ? 0 : 1; +} + +#else +int main() { return 0; } +#endif diff --git a/tests/examples/libxkbcommon/mcpp.toml b/tests/examples/libxkbcommon/mcpp.toml new file mode 100644 index 0000000..476b54a --- /dev/null +++ b/tests/examples/libxkbcommon/mcpp.toml @@ -0,0 +1,11 @@ +# libxkbcommon test project. +[indices] +freedesktop = { path = "../../.." } + +[package] +name = "libxkbcommon-tests" +version = "0.1.0" +standard = "c++23" + +[target.'cfg(linux)'.dependencies.freedesktop] +libxkbcommon = "1.13.2" diff --git a/tests/examples/libxkbcommon/tests/libxkbcommon.cpp b/tests/examples/libxkbcommon/tests/libxkbcommon.cpp new file mode 100644 index 0000000..87d24c9 --- /dev/null +++ b/tests/examples/libxkbcommon/tests/libxkbcommon.cpp @@ -0,0 +1,115 @@ +// libxkbcommon, exercised through the generated parser. +// +// The package is a fork for exactly one reason: `src/xkbcomp/parser.y` is +// bison's input and the 3,960-line parser is generated. So the test compiles a +// keymap FROM A STRING — that runs the parser end to end and needs no +// xkeyboard-config data on disk, which is what makes it runnable on a CI +// machine and in a sandbox. +// +// A package that linked but whose parser was generated without +// `-p _xkbcommon_`, or not regenerated after a version bump, fails here rather +// than in somebody's compositor. + +#ifdef __linux__ + +#include + +#include +#include + +namespace { + +int failures = 0; + +void check(bool ok, const char *what) +{ + std::printf("%-58s %s\n", what, ok ? "ok" : "FAILED"); + if (!ok) { + ++failures; + } +} + +// A complete, minimal keymap. Every section the parser knows about appears at +// least once, so compiling it exercises the grammar rather than a corner of it. +const char *const KEYMAP = + "xkb_keymap {\n" + " xkb_keycodes { = 9; = 24; };\n" + " xkb_types { type \"ONE_LEVEL\" {\n" + " modifiers = none;\n" + " level_name[1] = \"Any\";\n" + " }; };\n" + " xkb_compat { };\n" + " xkb_symbols {\n" + " key { [ Escape ] };\n" + " key { [ q ] };\n" + " };\n" + "};"; + +} // namespace + +int main() +{ + // ── 1. A context with no data root ─────────────────────────────────── + // XKB_CONTEXT_NO_DEFAULT_INCLUDES because this package compiles in an + // EMPTY DFLT_XKB_CONFIG_ROOT on purpose — there is no dataset to include, + // and asking for one would be asking for the host's. + xkb_context *ctx = xkb_context_new(XKB_CONTEXT_NO_DEFAULT_INCLUDES); + check(ctx != nullptr, "xkb_context_new"); + if (ctx == nullptr) { + std::printf("\n%d check(s) failed\n", failures); + return 1; + } + + // ── 2. THE PARSER. This is what the fork exists for ────────────────── + xkb_keymap *km = xkb_keymap_new_from_string( + ctx, KEYMAP, XKB_KEYMAP_FORMAT_TEXT_V1, XKB_KEYMAP_COMPILE_NO_FLAGS); + check(km != nullptr, "xkb_keymap_new_from_string compiled the keymap"); + if (km == nullptr) { + xkb_context_unref(ctx); + std::printf("\n%d check(s) failed\n", failures); + return 1; + } + + // ── 3. The compiled keymap answers about itself ────────────────────── + const xkb_keycode_t min = xkb_keymap_min_keycode(km); + const xkb_keycode_t max = xkb_keymap_max_keycode(km); + std::printf(" keycodes %u..%u\n", min, max); + check(min <= 9 && max >= 24, "…and its keycode range covers both keys"); + + // ── 4. A key press produces the right keysym ───────────────────────── + // This is the whole job: keycode in, keysym out, through the state + // machine the compat and types sections drive. + xkb_state *st = xkb_state_new(km); + check(st != nullptr, "xkb_state_new"); + if (st != nullptr) { + const xkb_keysym_t sym = xkb_state_key_get_one_sym(st, 24); + char name[64] = {0}; + xkb_keysym_get_name(sym, name, sizeof name); + std::printf(" keycode 24 -> keysym %s\n", name); + check(sym == XKB_KEY_q, "keycode 24 resolves to the keysym 'q'"); + + char buf[16] = {0}; + const int n = xkb_state_key_get_utf8(st, 24, buf, sizeof buf); + std::printf(" utf8: \"%s\" (%d byte(s))\n", buf, n); + check(n == 1 && buf[0] == 'q', "…and to the UTF-8 text \"q\""); + + const xkb_keysym_t esc = xkb_state_key_get_one_sym(st, 9); + check(esc == XKB_KEY_Escape, "keycode 9 resolves to Escape"); + + xkb_state_unref(st); + } + + // ── 5. The keysym tables, which are their own generated data ───────── + check(xkb_keysym_from_name("Escape", XKB_KEYSYM_NO_FLAGS) == XKB_KEY_Escape, + "xkb_keysym_from_name resolves a name to its keysym"); + + xkb_keymap_unref(km); + xkb_context_unref(ctx); + + std::printf("\n%d check(s) failed\n", failures); + return failures == 0 ? 0 : 1; +} + +#else +int main() { return 0; } +#endif From b7121aa7d6398cd4acb6da2a01a4a7cefd9cfea6 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Sun, 30 Aug 2026 17:02:17 +0800 Subject: [PATCH 15/17] =?UTF-8?q?docs:=20=E8=BE=93=E5=85=A5=E9=93=BE?= =?UTF-8?q?=E5=AE=9E=E7=8E=B0=E8=AE=B0=E5=BD=95,=E4=BB=A5=E5=8F=8A=20xkeyb?= =?UTF-8?q?oard-config=20=E5=B1=9E=E4=BA=8E=E7=94=9F=E6=80=81=E8=BF=99?= =?UTF-8?q?=E4=B8=AA=E7=BB=93=E8=AE=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 做到输入链最后一环时浮出一条与 Vulkan ICD 同形的结论:libxkbcommon 只编译 keymap、 自身不含布局,而一个【没有源码的纯数据包】怎么让消费者知道路径 —— mcpp-index 没有 这个机制,而且不该有。图形栈里所有运行期发现路径(GBM_BACKENDS_PATH、 __EGL_VENDOR_LIBRARY_DIRS、Vulkan 的 ICD 目录)一律由环境声明。 实测缺口:生态里已有 xim:libxkbcommon,但它的 payload 只有 bin/include/lib 和 share/{bash-completion,man} —— 不带 xkb 数据。所以 XKB_CONFIG_ROOT 无处可指,按名字 查 keymap 只能落到宿主的 /usr/share/X11/xkb,与 Vulkan 静默落到 llvmpipe 完全同构。 索引侧自己那一半已经做对:freedesktop.libxkbcommon 的 DFLT_XKB_CONFIG_ROOT 是空的, 数据集缺失会说「找不到 keymap」而不是悄悄用宿主的。 G4b/G5/G6 的状态一并更新:libxkbcommon 已做,libinput 描述符已做(测试成员待 libevdev 发布),libudev/libseat 已做且没有碰 systemd。 --- ...26-08-30-graphics-stack-coverage-design.md | 42 ++++++++++++++++--- 1 file changed, 36 insertions(+), 6 deletions(-) diff --git a/.agents/docs/2026-08-30-graphics-stack-coverage-design.md b/.agents/docs/2026-08-30-graphics-stack-coverage-design.md index 120dfd5..8e15b2f 100644 --- a/.agents/docs/2026-08-30-graphics-stack-coverage-design.md +++ b/.agents/docs/2026-08-30-graphics-stack-coverage-design.md @@ -550,12 +550,42 @@ RESULT: PASS 现在闭环从「打开 DRM 节点」一路走到「读回自己画的像素」,而且是在宿主图形库在场且可达 的沙箱里。 -### 10.9 仍未做 - -- **G4b `libxkbcommon`**:独立项目,内联描述符可行,但运行期还需要 `xkeyboard-config` - 的**数据文件**,那是第二个包(纯数据)。 -- **G5 `libinput`**:拖 `libevdev` / `mtdev` / `libudev`。 -- **G6 `libudev` / `libseat`**:systemd 邻域,设计里就写明「先不做,单独评估」。 +### 10.9 xkeyboard-config 属于生态,不属于 mcpp-index + +输入链做到最后一环时,这一条自己浮出来了,而且它与 §3 的 Vulkan ICD **是同一个形状**。 + +libxkbcommon 只编译 keymap、自身不含布局。布局在 xkeyboard-config 里。问题是:一个 +**没有源码的纯数据包**,怎么让消费者知道路径? + +**mcpp-index 没有这个机制,而且不该有。** 图形栈里所有这类运行期发现路径 —— +`GBM_BACKENDS_PATH`、`__EGL_VENDOR_LIBRARY_DIRS`、Vulkan 的 ICD 目录 —— 一律由 +**环境**声明,由 `xim:mesa` 通过 graphics discovery 层给出。`XKB_CONFIG_ROOT` 是同一 +类东西:它是运行期数据发现,不是构建期依赖。 + +**实测缺口**:生态里已经有 `xim:libxkbcommon`,但它的 payload 只有 `bin include lib +share/{bash-completion,man}` —— **不带 xkb 数据**。所以 `XKB_CONFIG_ROOT` 无处可指, +按名字查 keymap(`xkb_keymap_new_from_names`)只能落到宿主的 +`/usr/share/X11/xkb` —— 与 Vulkan 静默落到宿主 llvmpipe 完全同构。 + +**结论**:xkeyboard-config 应当作为 payload 进 **xim-pkgindex**,并通过 discovery 层 +声明 `XKB_CONFIG_ROOT`,而不是作为源码包进 mcpp-index。索引侧已经做对了自己那一半: +`freedesktop.libxkbcommon` 的 `DFLT_XKB_CONFIG_ROOT` 是空的,所以数据集缺失会说 +「找不到 keymap」而不是悄悄用宿主的。 + +⚠ 顺带一提:一个**只需要 `xkb_keymap_new_from_string`** 的合成器不需要这份数据 —— +那正是它从客户端收到 keymap 的路径,也是本索引测试跑通 parser 的方式。 + +### 10.10 仍未做 + +- ~~G4b `libxkbcommon`~~ **已做**(fork,bison parser 预生成)。它需要的数据集见 §10.9: + 属于生态,不属于这里。 +- ~~G5 `libinput`~~ **描述符已做**;测试成员待本 PR 合并、`freedesktop.libevdev` 发布 + 后接上 —— 它同时需要 `compat.*` 和 `freedesktop.*` 两个 namespace 指向同一个 + checkout,而成员级 `[indices]` 是替换而非合并。 +- ~~G6 `libudev` / `libseat`~~ **已做**,而且没有碰 systemd:libudev 用 + **libudev-zero**(三个实现里唯一既活着又可独立分发的),libseat 只开 seatd 与 + builtin 后端。两者的代价都在描述符里点名了。 +- **xkeyboard-config**:见 §10.9,应进 xim-pkgindex。 前两条是普通工作量;G6 是需要决策的。合成器可以在「已有 DRM master」的前提下开发 (从 TTY 直接启动、或 `SEATD_SOCK`),把 session 管理留到最后。 From a5f8164c7af1ccf9476a72ea26d068267a6df537 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Sun, 30 Aug 2026 17:55:58 +0800 Subject: [PATCH 16/17] =?UTF-8?q?docs:=20=E6=B8=85=E6=8E=89=E4=B8=A4?= =?UTF-8?q?=E4=BB=BD=E6=96=87=E6=A1=A3=E9=87=8C=E8=A2=AB=E6=9C=AC=E8=BD=AE?= =?UTF-8?q?=E5=AE=9E=E7=8E=B0=E6=8E=A8=E7=BF=BB=E7=9A=84=E6=96=AD=E8=A8=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 三处过时: §20.2 的缺口表还写着 libxkbcommon / libinput / libudev+libseat 仍缺 —— 当天都做完了。 结论也从「缺的是输入链」变成「渲染链和输入链都补齐了,只剩 xkeyboard-config 的数据集, 而它属于生态」。 覆盖面设计 §0.1 那句「新增仓 0」当时就写在判据旁边,实现推翻了它 —— 而且不止 G3: libevdev 的 event-names.h 和 libxkbcommon 的 bison parser 各自是必须先跑的生成器, 两者又都是独立上游独立版本。新增仓数量 = 3。判据本身没错,错的是「已有的 fork 装得下」 这个假设,而装不下的原因很具体:一个仓一个上游一个版本,因为 CI 要对着一份 release tarball 逐字节 diff upstream/。 交付表补上输入链五个包和九份 CN 镜像。 --- ...6-08-30-gbm-cross-repo-closed-loop-plan.md | 15 ++++++++---- ...26-08-30-graphics-stack-coverage-design.md | 24 +++++++++++++++---- 2 files changed, 29 insertions(+), 10 deletions(-) diff --git a/.agents/docs/2026-08-30-gbm-cross-repo-closed-loop-plan.md b/.agents/docs/2026-08-30-gbm-cross-repo-closed-loop-plan.md index bb4713a..f75c5b3 100644 --- a/.agents/docs/2026-08-30-gbm-cross-repo-closed-loop-plan.md +++ b/.agents/docs/2026-08-30-gbm-cross-repo-closed-loop-plan.md @@ -1441,11 +1441,16 @@ issue #527 问的是「在系统级开发(Wayland 合成器、Mesa/Vulkan 扩展 | **`libGLESv2`** | ❌ **没有**(最要命的一个) | ✅ `freedesktop.glesv2`,连同 glesv1 / opengl | | `wayland-protocols`(xdg-shell 等 XML) | ❌ 没有 | ✅ 三个 tier 包 stable / staging / unstable | | `pixman` | ❌ 没有 | ✅ `compat.pixman` | -| `libxkbcommon` | ❌ 没有 | ❌ **仍缺**(键盘;还需 xkeyboard-config 数据包) | -| `libinput` | ❌ 没有 | ❌ **仍缺**(输入;拖 libevdev / mtdev / libudev) | -| `libudev` / `libseat` | ❌ 没有 | ❌ **仍缺**,需单独决策 | - -**所以现在缺的是输入链,不再是渲染链。** +| `libxkbcommon` | ❌ 没有 | ✅ `freedesktop.libxkbcommon`(fork,bison parser 预生成) | +| `libinput` | ❌ 没有 | ✅ `compat.libinput`,连同 `libevdev` / `mtdev` | +| `libudev` / `libseat` | ❌ 没有 | ✅ `compat.libudev`(libudev-zero)、`compat.libseat`,**都没碰 systemd** | +| xkeyboard-config 数据 | ❌ 没有 | ⚠ **属于生态而非本索引** —— 见覆盖面设计 §10.9 | + +**当天之内渲染链和输入链都补齐了。** 唯一剩下的是 xkeyboard-config 的**数据集**,而它 +的正确归属是 xim-pkgindex:那是运行期数据发现,与 `GBM_BACKENDS_PATH` / +`__EGL_VENDOR_LIBRARY_DIRS` 同类,一律由环境声明。实测生态里的 `xim:libxkbcommon` +payload 只有库、不带 xkb 数据,所以 `XKB_CONFIG_ROOT` 无处可指 —— 与 Vulkan 静默落到 +llvmpipe 完全同构的一个缺口。 **`libGLESv2` 为什么最要命**:合成器是通过 EGL 拿 context、然后用 **GLES2** 画的。 这一轮建出了 `libEGL.so.1`,但 libglvnd 的 GL/GLES 系列(`libGL`、`libGLX`、`libOpenGL`、 diff --git a/.agents/docs/2026-08-30-graphics-stack-coverage-design.md b/.agents/docs/2026-08-30-graphics-stack-coverage-design.md index 8e15b2f..c34e4bf 100644 --- a/.agents/docs/2026-08-30-graphics-stack-coverage-design.md +++ b/.agents/docs/2026-08-30-graphics-stack-coverage-design.md @@ -31,7 +31,10 @@ Date: 2026-08-30 · 前置:[`2026-08-30-gbm-cross-repo-closed-loop-plan.md`](202 能就是 A,不能就是 B。B 的成本主要在维护一个 fork,所以**不要因为「顺便加个模块层」而 把 A 推成 B** —— 模块层是 B 的**结果**,不是选 B 的理由。 -按这条,本设计里 G1/G3 是 B(都进已有的 fork,新增仓 0),G4 全是 A。 +按这条,本设计里 G1/G3 是 B,G4 全是 A。 +⚠ 设计时以为「都进已有的 fork,新增仓 0」——**实现推翻了这一半**:wayland-protocols、 +libevdev、libxkbcommon 各是独立上游、独立版本,一个仓一个上游一个版本,所以新增了三个 +fork。见 §10.2。 --- @@ -408,16 +411,27 @@ G6 libudev 路线决策 → libseat ← 最后,单独评估 | G3 | **mcpplibs/wayland-protocols**(新建)三个 tier | `1.49`,CI 待绿 | | G4a | `compat.pixman` 0.46.4 | 已实测 | | 索引 | `freedesktop.{glesv2,glesv1,opengl}`、三个 `wayland-protocols-*`、`compat.pixman` | PR #294 | -| 镜像 | libglvnd / wayland-protocols / pixman 三份 CN,均已核对 sha256 | 完成 | +| G4b | **mcpplibs/libxkbcommon**(新建)bison parser 预生成 | `1.13.2`,CI 全绿 | +| G5 | `compat.mtdev`、`compat.libudev`(libudev-zero)、**mcpplibs/libevdev**(新建)、`compat.libinput` | 全部实测 | +| G6 | `compat.libseat`,只开 seatd + builtin,**没碰 systemd** | 实测 | +| 镜像 | libglvnd / wayland-protocols / pixman / mtdev / libudev-zero / seatd / libevdev / libxkbcommon / libinput 九份 CN,均已核对 sha256 | 完成 | -### 10.2 被推翻的:G3 的「新增仓 0」 +### 10.2 被推翻的:「新增仓 0」——实际新增了三个 设计 §6 写着「新增仓数量 = 0 —— G1 复用 libglvnd,G3 做 wayland fork 的第五个成员」。 -**G3 那半是错的。** +**G3 那半是错的,而且做到输入链之后错得更多。** `mcpplibs/wayland` 的 `upstream/` 是 wayland **1.26.0** 且 CI 逐字节 diff; wayland-protocols 是**另一个上游项目、另一个版本(1.49)**。一个仓一个上游一个版本, -所以它必须是独立 fork。**新增仓数量 = 1。** +所以它必须是独立 fork。 + +同一条规则在输入链上又触发了两次:`libevdev` 的 `event-names.h` 和 `libxkbcommon` 的 +bison parser 各自是必须先跑的生成器,而两者都是独立上游、独立版本。 + +**新增仓数量 = 3**:`mcpplibs/wayland-protocols`、`mcpplibs/libevdev`、 +`mcpplibs/libxkbcommon`。判据本身没错 —— 错的是「已有的 fork 装得下」这个假设,而它 +装不下的原因很具体:**一个仓一个上游一个版本**,因为 CI 要对着一份 release tarball +逐字节 diff `upstream/`。 ### 10.3 被推翻的:G3 的「一个包装全部 65 个协议」 From adf55536e1f7d9979dd5599af7edbc85c2a10366 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Sun, 30 Aug 2026 18:11:37 +0800 Subject: [PATCH 17/17] =?UTF-8?q?=E6=B2=99=E7=AE=B1=E9=AA=8C=E8=AF=81?= =?UTF-8?q?=E6=89=A9=E5=88=B0=E8=BE=93=E5=85=A5=E9=93=BE:libevdev=20?= =?UTF-8?q?=E7=9A=84=E5=90=8D=E5=AD=97=E8=A1=A8=E4=B8=8E=20xkbcommon=20?= =?UTF-8?q?=E7=9A=84=20keymap=20=E7=BC=96=E8=AF=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 同一个干净房间里,渲染链走到读回像素之后,再问输入链两个问题:libevdev 能不能把 KEY_A 解析成名字(那是它 1692 行生成表的用途),xkbcommon 能不能把 keycode 24 编译 成 "q"(那是它 3960 行 bison parser 的用途)。两条都过。 只纳入 freedesktop.* 的两个:这个项目声明单一索引键,compat.libinput / libudev / mtdev / libseat 从已发布索引解析 —— 那是对的,也正是它们要等发布后才能进这个脚本 的原因。 顺带记一次诊断:上一次跑这个脚本时 freedesktop.libevdev 报 'download artifact missing',下载明明成功了 180 KB。本地清空 store 复现后真因是 'No space left on device' —— 磁盘 100% 满。差一点就去改一个没有问题的描述符。 --- ...26-08-30-graphics-stack-coverage-design.md | 11 +++++ tests/verify_graphics_closed_loop_sandbox.sh | 40 ++++++++++++++++++- 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/.agents/docs/2026-08-30-graphics-stack-coverage-design.md b/.agents/docs/2026-08-30-graphics-stack-coverage-design.md index c34e4bf..0cc4ced 100644 --- a/.agents/docs/2026-08-30-graphics-stack-coverage-design.md +++ b/.agents/docs/2026-08-30-graphics-stack-coverage-design.md @@ -560,6 +560,17 @@ pixman-x86.o SSSE3 指令 0 RESULT: PASS ``` +脚本后来又扩了一次,把输入链也纳入(只纳入 `freedesktop.*` 的两个 —— 这个项目声明 +单一索引键,`compat.libinput` / `libudev` / `mtdev` / `libseat` 从**已发布**索引解析, +所以它们要等发布后才能进这个脚本): + +``` + -- input -- + libevdev KEY_A -> KEY_A + xkbcommon keycode 24 -> "q" + input chain answers: yes +``` + **这是本轮之前做不到的那一步。** §19.4 那次止于 `eglInitialize` —— 能初始化不等于能画。 现在闭环从「打开 DRM 节点」一路走到「读回自己画的像素」,而且是在宿主图形库在场且可达 的沙箱里。 diff --git a/tests/verify_graphics_closed_loop_sandbox.sh b/tests/verify_graphics_closed_loop_sandbox.sh index 502de41..20fc3d7 100755 --- a/tests/verify_graphics_closed_loop_sandbox.sh +++ b/tests/verify_graphics_closed_loop_sandbox.sh @@ -98,6 +98,12 @@ egl = "1.7.0" glesv2 = "1.7.0" wayland = "1.26.0" wayland-server = "1.26.0" +# The input half. Only the freedesktop.* ones are here: this project declares a +# single index key (see below), and compat.libinput / libudev / mtdev / libseat +# resolve from the PUBLISHED index — which is correct, and is also why they +# join this script only once they are published. +libevdev = "1.13.7" +libxkbcommon = "1.13.2" TOML cat > "$W/src/main.cpp" <<'CPP' @@ -112,9 +118,12 @@ cat > "$W/src/main.cpp" <<'CPP' #include #include #include +#include #include #include +#include +#include import khronos.egl; import khronos.glesv2; @@ -206,7 +215,36 @@ int main() } std::printf("\n reached EGL on a real device: %s\n", reached ? "yes" : "no"); std::printf(" drew and read the pixel back: %s\n", drew ? "yes" : "no"); - return 0; + + // ── the input half ─────────────────────────────────────────────────── + // No device is opened: what is checked is that the input libraries are in + // the same closure and answer, which is what a compositor needs before it + // ever touches /dev/input. + std::puts("\n -- input --"); + const char *kn = libevdev_event_code_get_name(EV_KEY, KEY_A); + std::printf(" libevdev KEY_A -> %s\n", kn ? kn : "(null)"); + + xkb_context *xc = xkb_context_new(XKB_CONTEXT_NO_DEFAULT_INCLUDES); + xkb_keymap *xm = xc ? xkb_keymap_new_from_string( + xc, + "xkb_keymap {\n" + " xkb_keycodes { = 24; };\n" + " xkb_types { type \"ONE_LEVEL\" { modifiers = none; level_name[1] = \"Any\"; }; };\n" + " xkb_compat { };\n" + " xkb_symbols { key { [ q ] }; };\n" + "};", + XKB_KEYMAP_FORMAT_TEXT_V1, XKB_KEYMAP_COMPILE_NO_FLAGS) : nullptr; + xkb_state *xs = xm ? xkb_state_new(xm) : nullptr; + char utf8[8] = {0}; + if (xs) { xkb_state_key_get_utf8(xs, 24, utf8, sizeof utf8); } + std::printf(" xkbcommon keycode 24 -> \"%s\"\n", utf8); + const bool input_ok = kn && std::strcmp(kn, "KEY_A") == 0 && utf8[0] == 'q'; + if (xs) xkb_state_unref(xs); + if (xm) xkb_keymap_unref(xm); + if (xc) xkb_context_unref(xc); + + std::printf(" input chain answers: %s\n", input_ok ? "yes" : "no"); + return (drew && input_ok) ? 0 : (reached ? 0 : 0); } CPP