Skip to content

Commit de747d4

Browse files
committed
docs(examples): 09 —— 系统级图形栈,用推荐方式做完整条链 (#527)
#527 §1 的问法是「宿主库直接动态链接能不能原样用」,拿 `-L/usr/lib -lgbm` 做复现。这个方向是错的:mcpp 运行时不依赖 Host 是设计决策(#527 A3),产物 拿到的是 mcpp 自己算出搜索路径的私有 PT_INTERP,所以 `-L/usr/lib` 不是缺失 的支持,而是错误用法,mcpp 在构建期就会说出来。 该回答的是功能需求本身能不能做到。这个示例就是答案:合成器 / Mesa 面向的 扩展 / GBM 显存管理所需要的那条链,按推荐方式声明依赖跑通。 [target.'cfg(linux)'.dependencies.compat] libgbm = "2026.08.29" libdrm = "2026.08.30" egl = "2026.08.30" wayland = "2026.08.30" 这是全部配置。src/main.cpp 用的是上游原样的头和 API,没有任何 mcpp 特有的 东西 —— 别处照着这些库写的代码搬过来就能编。 它做的是真事而不是「符号能链上」:打开 /dev/dri/renderD128,由该 fd 建出真 的 gbm_device,交给 eglGetPlatformDisplay(EGL_PLATFORM_GBM_KHR, ...) 并初始 化 EGL。实测两个节点(nvidia-drm / simpledrm)都到 EGL 1.5, vendor Mesa Project。对照 §1.2 里的 gbm_create_device(-1) 返回 NULL —— 那个用例即使在 宿主上跑通也没证明栈可用。 零 Host 是核过的,不是断言的:用产物自己的私有加载器解析完整闭包,11 条全部 落在 registry 内,没有一条在 /usr/lib 或 /lib64。其中 libexpat、libffi、 libGLdispatch 是传递依赖,mcpp.toml 里谁都没写 —— 而 §1.4 断言的正是这层 级联在私有加载器里解不开。声明四个依赖把 11 条都解掉了。 README 另外如实记了一处缺口:Vulkan 不在本示例内,形态也不同 —— compat.vulkan-runtime 仍从 /usr/lib/*、/lib64 收割宿主 ICD,因为 Vulkan 驱动 属于 GPU 厂商,生态里还没有可绑的 payload。GBM/KMS/EGL/Wayland 这条没有这 个边。
1 parent ab1da5d commit de747d4

3 files changed

Lines changed: 281 additions & 0 deletions

File tree

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
# 09 — System Graphics Without the Host
2+
3+
A KMS/DRM program that opens a GPU, allocates buffers and brings up EGL — with
4+
nothing from `/usr/lib` and no host loader.
5+
6+
```bash
7+
mcpp run
8+
```
9+
10+
```
11+
== the graphics stack, resolved from the index ==
12+
GBM_BACKENDS_PATH = …/subos/default/usr/lib/gbm
13+
wl_display_create 0x2e8d8be0
14+
-- DRM node -> GBM device -> EGL display --
15+
/dev/dri/renderD128
16+
drm driver nvidia-drm
17+
gbm_create_device 0x2e942f30
18+
eglGetPlatformDisplay 0x2e9bd390
19+
eglInitialize EGL 1.5, vendor Mesa Project
20+
done.
21+
```
22+
23+
## What this example is for
24+
25+
System-level graphics work — a Wayland compositor, a Mesa-facing extension,
26+
GBM buffer management — is the case people expect a managed runtime to be bad
27+
at, because it is the case where "just link the system library" is the reflex.
28+
29+
mcpp's runtime deliberately does not depend on the host: that is what makes a
30+
build reproducible and portable across distributions, and it is why an artifact
31+
gets a private `PT_INTERP` whose search path mcpp computed rather than the
32+
host's `/etc/ld.so.cache`. So `-L/usr/lib -lgbm` is not a thing mcpp is missing
33+
support for — it is the wrong way to ask, and mcpp says so at build time.
34+
35+
The right question is whether the **work** can be done. This example is the
36+
answer: the whole chain, done the recommended way, declaring dependencies.
37+
38+
```toml
39+
[target.'cfg(linux)'.dependencies.compat]
40+
libgbm = "2026.08.29"
41+
libdrm = "2026.08.30"
42+
egl = "2026.08.30"
43+
wayland = "2026.08.30"
44+
```
45+
46+
That is the entire configuration. `src/main.cpp` then includes `<gbm.h>`,
47+
`<xf86drm.h>`, `<EGL/egl.h>` and `<wayland-client.h>` and calls the stock
48+
upstream APIs — nothing in it is mcpp-specific, so code written against these
49+
libraries anywhere else compiles here unchanged.
50+
51+
And it does the real thing rather than proving a symbol resolves: it opens
52+
`/dev/dri/renderD128`, builds a genuine `gbm_device` from that fd, hands it to
53+
`eglGetPlatformDisplay(EGL_PLATFORM_GBM_KHR, …)` and initializes EGL against a
54+
real driver. A `gbm_create_device(-1)` on an invalid fd returns `NULL` and
55+
tells you nothing about whether the stack works; this reaches
56+
`EGL 1.5, vendor Mesa Project`.
57+
58+
## Checking the claim
59+
60+
"Host-free" is easy to assert, so it is worth resolving the artifact's closure
61+
through the private loader it actually uses and looking at every path:
62+
63+
```bash
64+
BIN=target/x86_64-linux-gnu/*/bin/graphics-stack
65+
"$(readelf -p .interp $BIN | grep -o '/.*ld-linux[^ ]*')" --list $BIN
66+
```
67+
68+
```
69+
compat-x-egl/2026.08.30/…/libEGL.so.1
70+
compat-x-libdrm/2026.08.30/…/libdrm.so.2
71+
compat-x-libgbm/2026.08.29/…/libgbm.so.1
72+
compat-x-wayland/2026.08.30/…/libwayland-client.so.0
73+
compat-x-wayland/2026.08.30/…/libwayland-server.so.0
74+
xim-x-expat/2.6.2/lib/libexpat.so.1
75+
xim-x-gcc/16.1.0/lib64/libgcc_s.so.1
76+
xim-x-glibc/2.44/lib64/libc.so.6
77+
xim-x-glibc/2.44/lib64/libm.so.6
78+
xim-x-libffi/3.4.4/lib/libffi.so.8
79+
xim-x-libglvnd/1.7.0.1/lib/libGLdispatch.so.0
80+
```
81+
82+
Every entry is under the registry; none is under `/usr/lib` or `/lib64`. Note
83+
the bottom half especially — `libexpat`, `libffi` and `libGLdispatch` are
84+
*transitive*: nothing in `mcpp.toml` names them. They are what a directly
85+
linked `libgbm.so.1` cascades into, and resolving that cascade is exactly what
86+
the host path cannot do from inside a private loader. Declaring the four
87+
dependencies resolved all eleven.
88+
89+
## The packages
90+
91+
None of them vendors a source tree. Mesa, libdrm, libglvnd and wayland are
92+
already in the ecosystem (`xim:mesa`, `xim:libdrm`, `xim:libglvnd`,
93+
`xim:wayland`), so each package is a thin binding: it declares the ecosystem
94+
package it needs and exposes that payload's headers and libraries to the
95+
compiler. Building second copies would put two `libgbm.so.1` — or two
96+
`libdrm.so.2`, or a second EGL dispatch library — in a process that already
97+
loads Mesa's.
98+
99+
| package | what it gives you |
100+
|---|---|
101+
| `compat.libgbm` | `gbm_create_device`, `gbm_bo_create` — buffers out of a DRM device |
102+
| `compat.libdrm` | `drmModeGetResources`, `drmModeAddFB2`, `drmModeSetCrtc` — the KMS side |
103+
| `compat.egl` | `eglGetPlatformDisplay(EGL_PLATFORM_GBM_KHR, …)` — rendering onto them |
104+
| `compat.wayland` | client and server libraries for the display protocol |
105+
106+
One honest gap, since "Mesa/Vulkan" usually get named together: Vulkan is not
107+
part of this example and is not in the same state. `compat.vulkan-runtime`
108+
builds its farm by harvesting the host's ICDs out of `/usr/lib/*` and `/lib64`,
109+
because a Vulkan driver is the GPU vendor's and there is no ecosystem payload
110+
to bind to yet. The GBM/KMS/EGL/Wayland stack above has no such edge.
111+
112+
## Two things worth knowing
113+
114+
**`GBM_BACKENDS_PATH` is not set by any of these packages.** libgbm is a
115+
loader: `gbm_create_device()` dlopens `<path>/<driver>_gbm.so`, and the path
116+
Mesa compiles in is `/usr/lib/gbm` — correct on a distribution, wrong the
117+
moment the payload lives anywhere else. Setting that variable is Mesa's own
118+
mechanism and the *environment's* job, which is where every relocated stack
119+
puts it (Valve's pressure-vessel, Nix, Conda all do exactly this). Here
120+
`xim:mesa` declares it into the SubOS and mcpp carries SubOS declarations into
121+
the processes it launches, so it is simply already set — which is why the
122+
program prints it rather than computing it.
123+
124+
**`compat.wayland` puts only `-lwayland-client` on the link line**, and this
125+
example adds the other half itself:
126+
127+
```toml
128+
[target.'cfg(linux)'.build]
129+
ldflags = ["-lwayland-server"]
130+
```
131+
132+
A dependency's `ldflags` reach every consumer with no way to opt out, so a
133+
package that forced `libwayland-server` on every client would be unfixable
134+
downstream. All four wayland libraries are present; a compositor asks for the
135+
one it needs and it resolves out of the same package.
136+
137+
## Running it
138+
139+
The DRM section needs a GPU. On a machine without one — a container, most CI
140+
runners — the program says so and everything that does not need hardware has
141+
already run:
142+
143+
```
144+
(no DRM node reached EGL — expected without a GPU)
145+
```
146+
147+
To watch the backend loader itself, ask Mesa:
148+
149+
```bash
150+
EGL_LOG_LEVEL=debug mcpp run
151+
```
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
[package]
2+
name = "graphics-stack"
3+
version = "0.1.0"
4+
5+
# The whole KMS/DRM stack, from the index. No host paths, no -L/usr/lib.
6+
[target.'cfg(linux)'.dependencies.compat]
7+
libgbm = "2026.08.29" # buffer allocation out of a DRM device
8+
libdrm = "2026.08.30" # the KMS side: modes, CRTCs, framebuffers
9+
egl = "2026.08.30" # rendering onto those buffers
10+
wayland = "2026.08.30" # the display protocol, client and server
11+
12+
# compat.wayland puts only -lwayland-client on the link line, because a
13+
# dependency's ldflags reach every consumer with no way to opt out. A
14+
# compositor asks for the server library itself; it resolves out of the same
15+
# package.
16+
[target.'cfg(linux)'.build]
17+
ldflags = ["-lwayland-server"]
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
// System-level graphics with no host dependency.
2+
//
3+
// This is the sequence a KMS/DRM program or a Wayland compositor actually
4+
// runs. Every header here is the stock upstream one and every call is the
5+
// stock upstream API — there is nothing mcpp-specific in this file, which is
6+
// the point: code written against these libraries anywhere else compiles here
7+
// unchanged.
8+
9+
#include <gbm.h> // compat.libgbm
10+
#include <xf86drm.h> // compat.libdrm
11+
#include <xf86drmMode.h>
12+
#include <drm_fourcc.h>
13+
#include <EGL/egl.h> // compat.egl
14+
#include <EGL/eglext.h>
15+
#include <wayland-client.h> // compat.wayland
16+
#include <wayland-server-core.h>
17+
18+
#include <cstdio>
19+
#include <cstdlib>
20+
#include <cstring>
21+
#include <initializer_list>
22+
23+
#include <fcntl.h>
24+
#include <unistd.h>
25+
26+
namespace {
27+
28+
// The two APIs exchange this value across the gbm_bo -> drmModeAddFB2
29+
// boundary. If the packages ever disagreed about it, a display would show the
30+
// wrong colours and nothing would report an error — so it is worth asserting
31+
// rather than assuming.
32+
static_assert(GBM_FORMAT_XRGB8888 == DRM_FORMAT_XRGB8888,
33+
"libgbm and libdrm must agree on the XRGB8888 fourcc");
34+
35+
void report(const char *label, const void *p)
36+
{
37+
std::printf(" %-24s %p\n", label, p);
38+
}
39+
40+
} // namespace
41+
42+
int main()
43+
{
44+
std::puts("== the graphics stack, resolved from the index ==");
45+
46+
// Where the GBM backends are found. Nothing in this program and nothing in
47+
// compat.libgbm sets this: `xim:mesa` declares it into the SubOS through
48+
// the graphics discovery layer, and mcpp carries SubOS declarations into
49+
// the processes it launches.
50+
const char *backends = std::getenv("GBM_BACKENDS_PATH");
51+
std::printf(" GBM_BACKENDS_PATH = %s\n",
52+
backends ? backends : "<unset — the ecosystem did not supply it>");
53+
54+
// Wayland: build a server-side display. No socket is bound, so this needs
55+
// no session and no privileges — the cheapest proof the library is live.
56+
if (wl_display *server = wl_display_create()) {
57+
report("wl_display_create", server);
58+
wl_display_destroy(server);
59+
} else {
60+
std::puts(" wl_display_create FAILED");
61+
return 1;
62+
}
63+
64+
// The real chain: a DRM node becomes a GBM device, which becomes an EGL
65+
// display. This is what "headless GPU rendering" means concretely, and it
66+
// is the sequence that cannot be expressed without all three packages.
67+
std::puts("-- DRM node -> GBM device -> EGL display --");
68+
bool reached_egl = false;
69+
70+
for (const char *node : {"/dev/dri/renderD128", "/dev/dri/card0"}) {
71+
const int fd = ::open(node, O_RDWR);
72+
if (fd < 0) {
73+
std::printf(" %-24s (not present on this machine)\n", node);
74+
continue;
75+
}
76+
std::printf(" %s\n", node);
77+
78+
if (drmVersionPtr v = drmGetVersion(fd)) {
79+
std::printf(" %-24s %s\n", "drm driver", v->name);
80+
drmFreeVersion(v);
81+
}
82+
83+
if (gbm_device *gbm = gbm_create_device(fd)) {
84+
report("gbm_create_device", gbm);
85+
86+
EGLDisplay dpy =
87+
eglGetPlatformDisplay(EGL_PLATFORM_GBM_KHR, gbm, nullptr);
88+
report("eglGetPlatformDisplay", dpy);
89+
90+
if (dpy != EGL_NO_DISPLAY) {
91+
EGLint major = 0, minor = 0;
92+
if (eglInitialize(dpy, &major, &minor)) {
93+
std::printf(" %-24s EGL %d.%d, vendor %s\n", "eglInitialize",
94+
major, minor, eglQueryString(dpy, EGL_VENDOR));
95+
reached_egl = true;
96+
eglTerminate(dpy);
97+
}
98+
}
99+
gbm_device_destroy(gbm);
100+
}
101+
::close(fd);
102+
}
103+
104+
if (!reached_egl) {
105+
// Not a failure of the packages: a machine with no DRM node (a
106+
// container, most CI runners) legitimately gets here. Everything above
107+
// that does not need hardware has already run.
108+
std::puts(" (no DRM node reached EGL — expected without a GPU)");
109+
}
110+
111+
std::puts("done.");
112+
return 0;
113+
}

0 commit comments

Comments
 (0)