|
| 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 | +``` |
0 commit comments