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 05ce84a1..f75c5b31 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 | 判据是「依不依赖目标平台」 | @@ -1214,6 +1215,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,12 +1395,108 @@ 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 的破坏随之修复并已复验。 +- 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 合成器** | ⚠️ 盘点时只覆盖协议库那一层;**当天下午补齐了渲染链**(GLESv2 + wayland-protocols + pixman),**输入链仍缺**(libxkbcommon / libinput / libseat) | 见 20.2 与覆盖面设计 §10 | +| **Mesa/Vulkan 扩展** | ❌ **Vulkan 侧仍伸手够宿主** | `compat.vulkan-runtime` 明写着它把宿主 `/usr/lib` 的 ICD 做成符号链接农场,是这个栈里最后一处 host 边 | + +**所以对 #527 的诚实回答是**:`-lgbm` 那个具体诉求,推荐方式**完全能覆盖且已证明**; +但把「系统级开发」整体说成已覆盖,是不成立的。 + +### 20.2 Wayland 合成器缺什么(逐个点名) + +一个真实合成器(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`** | ❌ **没有**(最要命的一个) | ✅ `freedesktop.glesv2`,连同 glesv1 / opengl | +| `wayland-protocols`(xdg-shell 等 XML) | ❌ 没有 | ✅ 三个 tier 包 stable / staging / unstable | +| `pixman` | ❌ 没有 | ✅ `compat.pixman` | +| `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`、 +`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, +> 还有一处宿主依赖没拆。** + +把「能不能用」答成「能」是不诚实的,答成「不能」也不对。准确的说法是: +**这一轮把可行性证明了,覆盖面还没到。** 缺口已经点名、形态已经确定、排序已经给出。 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 00000000..0cc4ced3 --- /dev/null +++ b/.agents/docs/2026-08-30-graphics-stack-coverage-design.md @@ -0,0 +1,616 @@ +# 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;模块名跟接口所有者。三条都在前一份文档里立过并验证过。 + +### 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,G4 全是 A。 +⚠ 设计时以为「都进已有的 fork,新增仓 0」——**实现推翻了这一半**:wayland-protocols、 +libevdev、libxkbcommon 各是独立上游、独立版本,一个仓一个上游一个版本,所以新增了三个 +fork。见 §10.2。 + +--- + +## 1. 调研结论速览 + +| # | 目标 | 调研结论 | 规模 | +|---|---|---|---| +| **G1** | GL 家族(GLESv2 等) | ✅ **形态完全确定**,四张生成表已实跑通过 | 中,收益最大 | +| **G2** | Vulkan 走生态驱动 | ⚠️ **原假设两次被推翻**,最终是「既有机制的 Vulkan 那一半没接」 | 一处调用,在 xim-pkgindex | +| **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 根因:机制早就在,只是 Vulkan 这一半没接 + +生态里**已经有一套完整的「共享 vendor 目录」设计**,而且已经在 EGL 上跑通了。不需要 +新机制,需要的是把 Vulkan 接进同一套。 + +`libs/graphics.lua` 与 `nvidia-gl-host-link.lua` 里的既有形态: + +``` +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」。 + +**所以设计原则是:host 面保留,但收敛到 sentinel,并且只对不开源的那一层。** +V2 要做的就是把 `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 nvidia-gl-host-link 增调 declare_vulkan_icd ← 无前置,与 G1 并行(xim-pkgindex) + │ + └─ G2b compat.vulkan-runtime 归类为「专有驱动的最小 host 面」 ← 依赖 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 | ✅ **已验证**(见 §3.4):`deviceName` 从 `llvmpipe` 变成 `NVIDIA GeForce RTX 4080`。做成 V1 之后应固化为沙箱回归 | +| 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,不在这里。** + +--- + +## 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 | +| 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 被推翻的:「新增仓 0」——实际新增了三个 + +设计 §6 写着「新增仓数量 = 0 —— G1 复用 libglvnd,G3 做 wayland fork 的第五个成员」。 +**G3 那半是错的,而且做到输入链之后错得更多。** + +`mcpplibs/wayland` 的 `upstream/` 是 wayland **1.26.0** 且 CI 逐字节 diff; +wayland-protocols 是**另一个上游项目、另一个版本(1.49)**。一个仓一个上游一个版本, +所以它必须是独立 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 个协议」 + +设计说「全量预生成」,只提了体积。**体积不是问题,链接才是。** + +``` +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 +``` + +脚本后来又扩了一次,把输入链也纳入(只纳入 `freedesktop.*` 的两个 —— 这个项目声明 +单一索引键,`compat.libinput` / `libudev` / `mtdev` / `libseat` 从**已发布**索引解析, +所以它们要等发布后才能进这个脚本): + +``` + -- input -- + libevdev KEY_A -> KEY_A + xkbcommon keycode 24 -> "q" + input chain answers: yes +``` + +**这是本轮之前做不到的那一步。** §19.4 那次止于 `eglInitialize` —— 能初始化不等于能画。 +现在闭环从「打开 DRM 节点」一路走到「读回自己画的像素」,而且是在宿主图形库在场且可达 +的沙箱里。 + +### 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 管理留到最后。 diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index d3425aa2..db5b96ac 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 ─────────────────────── diff --git a/mcpp.toml b/mcpp.toml index de10c0d1..60a371e3 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -86,6 +86,14 @@ members = [ "tests/examples/expat", "tests/examples/wayland", "tests/examples/egl", + "tests/examples/gl", + "tests/examples/libevdev", + "tests/examples/libseat", + "tests/examples/libudev", + "tests/examples/libxkbcommon", + "tests/examples/mtdev", + "tests/examples/pixman", + "tests/examples/wayland-protocols", "tests/examples/libgbm", "tests/examples/libpng", "tests/examples/libwebp", diff --git a/pkgs/c/compat.libinput.lua b/pkgs/c/compat.libinput.lua new file mode 100644 index 00000000..ada2e398 --- /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/c/compat.libseat.lua b/pkgs/c/compat.libseat.lua new file mode 100644 index 00000000..b42752d5 --- /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/pkgs/c/compat.libudev.lua b/pkgs/c/compat.libudev.lua new file mode 100644 index 00000000..5ef79659 --- /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 00000000..19f8c1f0 --- /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/c/compat.pixman.lua b/pkgs/c/compat.pixman.lua new file mode 100644 index 00000000..ff9e21f6 --- /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/pkgs/f/freedesktop.egl.lua b/pkgs/f/freedesktop.egl.lua index 43611b00..309da73d 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 00000000..05aa22d5 --- /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 00000000..5e61ff55 --- /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.libevdev.lua b/pkgs/f/freedesktop.libevdev.lua new file mode 100644 index 00000000..63623266 --- /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/pkgs/f/freedesktop.libxkbcommon.lua b/pkgs/f/freedesktop.libxkbcommon.lua new file mode 100644 index 00000000..ae1043b8 --- /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/pkgs/f/freedesktop.opengl.lua b/pkgs/f/freedesktop.opengl.lua new file mode 100644 index 00000000..2bbb0425 --- /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/pkgs/f/freedesktop.wayland-protocols-stable.lua b/pkgs/f/freedesktop.wayland-protocols-stable.lua new file mode 100644 index 00000000..4e1b3c22 --- /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 00000000..37e3862f --- /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 00000000..e5120f73 --- /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/gl/mcpp.toml b/tests/examples/gl/mcpp.toml new file mode 100644 index 00000000..a1e36c0d --- /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 00000000..293eb5a3 --- /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 diff --git a/tests/examples/libevdev/mcpp.toml b/tests/examples/libevdev/mcpp.toml new file mode 100644 index 00000000..ace9be95 --- /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 00000000..0de6f1dd --- /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/libinput/tests/libinput.cpp b/tests/examples/libinput/tests/libinput.cpp new file mode 100644 index 00000000..032be246 --- /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/libseat/mcpp.toml b/tests/examples/libseat/mcpp.toml new file mode 100644 index 00000000..01ce5c56 --- /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 00000000..60b29e18 --- /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 diff --git a/tests/examples/libudev/mcpp.toml b/tests/examples/libudev/mcpp.toml new file mode 100644 index 00000000..bc264b35 --- /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 00000000..9871716e --- /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/libxkbcommon/mcpp.toml b/tests/examples/libxkbcommon/mcpp.toml new file mode 100644 index 00000000..476b54ae --- /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 00000000..87d24c94 --- /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 diff --git a/tests/examples/mtdev/mcpp.toml b/tests/examples/mtdev/mcpp.toml new file mode 100644 index 00000000..1447277a --- /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 00000000..e282dce5 --- /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 diff --git a/tests/examples/pixman/mcpp.toml b/tests/examples/pixman/mcpp.toml new file mode 100644 index 00000000..51d0bf76 --- /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 00000000..7584d899 --- /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 diff --git a/tests/examples/wayland-protocols/mcpp.toml b/tests/examples/wayland-protocols/mcpp.toml new file mode 100644 index 00000000..e51aa314 --- /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 00000000..19e50ca1 --- /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 diff --git a/tests/verify_graphics_closed_loop_sandbox.sh b/tests/verify_graphics_closed_loop_sandbox.sh index 07fadcd7..20fc3d73 100755 --- a/tests/verify_graphics_closed_loop_sandbox.sh +++ b/tests/verify_graphics_closed_loop_sandbox.sh @@ -95,8 +95,15 @@ 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" +# 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' @@ -111,9 +118,15 @@ cat > "$W/src/main.cpp" <<'CPP' #include #include #include +#include #include +#include +#include +#include + import khronos.egl; +import khronos.glesv2; import freedesktop.wayland.client; import freedesktop.wayland.server; @@ -129,7 +142,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 +168,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,7 +214,37 @@ int main() ::close(fd); } std::printf("\n reached EGL on a real device: %s\n", reached ? "yes" : "no"); - return 0; + std::printf(" drew and read the pixel back: %s\n", drew ? "yes" : "no"); + + // ── 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 @@ -175,14 +256,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"