Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/01-examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ examples.
| 02 | [`examples/02-with-deps`](../examples/02-with-deps/) | Adds the `mcpplibs.cmdline` dependency to parse command-line arguments | `[dependencies]`, SemVer, `mcpp.lock` |
| 03 | [`examples/03-pack-static`](../examples/03-pack-static/) | Produces a fully static release package via `mcpp pack --mode static` | `[target.<triple>]` and `[pack]` configuration |
| 08 | [`examples/08-build-rules`](../examples/08-build-rules/) | Two rule packages and a project that uses both | `host-module = true`, `[build-dependencies]`, `mcpp::action` with `role = "check"` |
| 09 | [`examples/09-graphics-stack`](../examples/09-graphics-stack/) | KMS/DRM -> GBM -> EGL, plus Wayland, with nothing from the host | `compat.libgbm` / `libdrm` / `egl` / `wayland`, SubOS-supplied `GBM_BACKENDS_PATH` |

## Suggested Reading Order

Expand Down
1 change: 1 addition & 0 deletions docs/zh/01-examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ mcpp build && mcpp run
| 02 | [`examples/02-with-deps`](../../examples/02-with-deps/) | 引入依赖 `mcpplibs.cmdline` 解析命令行参数 | `[dependencies]`、SemVer、`mcpp.lock` |
| 03 | [`examples/03-pack-static`](../../examples/03-pack-static/) | 通过 `mcpp pack --mode static` 生成全静态发布包 | `[target.<triple>]` 与 `[pack]` 配置 |
| 08 | [`examples/08-build-rules`](../../examples/08-build-rules/) | 两个规则包,以及同时用到它们的工程 | `host-module = true`、`[build-dependencies]`、`role = "check"` 的 `mcpp::action` |
| 09 | [`examples/09-graphics-stack`](../../examples/09-graphics-stack/) | KMS/DRM -> GBM -> EGL 加 Wayland,不碰宿主的任何东西 | `compat.libgbm` / `libdrm` / `egl` / `wayland`、由 SubOS 提供的 `GBM_BACKENDS_PATH` |

## 推荐阅读顺序

Expand Down
246 changes: 246 additions & 0 deletions examples/09-graphics-stack/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,246 @@
# 09 — System Graphics Without the Host

A KMS/DRM program that opens a GPU, allocates buffers and brings up EGL — with
nothing from `/usr/lib` and no host loader.

```bash
mcpp run
```

```
== the graphics stack, resolved from the index ==
GBM_BACKENDS_PATH = …/subos/default/usr/lib/gbm
__EGL_VENDOR_LIBRARY_DIRS = …/subos/default/share/glvnd/egl_vendor.d
wl_display_create 0x3f798be0
-- DRM node -> GBM device -> EGL display --
/dev/dri/renderD128
drm driver nvidia-drm
gbm_create_device 0x3f802f30
gbm_bo_create (driver declined this format/usage)
eglGetPlatformDisplay 0x3f87d5b0
eglInitialize EGL 1.5, vendor Mesa Project
/dev/dri/card0
drm driver simpledrm
gbm_create_device 0x3f802f30
gbm_bo_create 256x256 stride=1024 modifier=0xffffffffffffff
eglGetPlatformDisplay 0x3f87d5b0
eglInitialize EGL 1.5, vendor Mesa Project
done.
```

That is one real run on a two-node machine, kept unabridged because the
difference between the nodes is the point: `simpledrm` allocated the buffer and
reported the driver's own stride and modifier, while NVIDIA's GBM backend
declined that format/usage combination. Both are the libraries answering — this
is the stack working, not a smoke test.

## What this example is for

System-level graphics work — a Wayland compositor, a Mesa-facing extension,
GBM buffer management — is the case people expect a managed runtime to be bad
at, because it is the case where "just link the system library" is the reflex.

mcpp's runtime deliberately does not depend on the host: that is what makes a
build reproducible and portable across distributions, and it is why an artifact
gets a private `PT_INTERP` whose search path mcpp computed rather than the
host's `/etc/ld.so.cache`. So `-L/usr/lib -lgbm` is not a thing mcpp is missing
support for — it is the wrong way to ask, and mcpp says so at build time.

The right question is whether the **work** can be done. This example is the
answer: the whole chain, done the recommended way, declaring dependencies.

```toml
[target.'cfg(linux)'.dependencies.compat]
libdrm = "2.4.134"
libgbm = "25.0.7"

[target.'cfg(linux)'.dependencies.freedesktop]
egl = "1.7.0"
wayland = "1.26.0"
wayland-server = "1.26.0"
```

That is the entire configuration. `src/main.cpp` then includes `<gbm.h>`,
`<xf86drm.h>`, `<EGL/egl.h>` and `<wayland-client.h>` and calls the stock
upstream APIs — nothing in it is mcpp-specific, so code written against these
libraries anywhere else compiles here unchanged.

And it does the real thing rather than proving a symbol resolves: it opens a
DRM node, builds a genuine `gbm_device` from that fd, **allocates actual GPU
memory** with `gbm_bo_create` and reads back the stride and modifier the driver
chose, then hands the device to
`eglGetPlatformDisplay(EGL_PLATFORM_GBM_KHR, …)` and initializes EGL. A
`gbm_create_device(-1)` on an invalid fd returns `NULL` and tells you nothing
about whether the stack works; this reaches `EGL 1.5, vendor Mesa Project`.

## Checking the claim

"Host-free" is easy to assert, so resolve the artifact's closure through the
private loader it actually uses and look at every path:

```bash
BIN=target/x86_64-linux-gnu/*/bin/graphics-stack
"$(readelf -p .interp $BIN | grep -o '/.*ld-linux[^ ]*')" --list $BIN
```

```
<project>/bin/libdrm.so.2 <- built by this build
<project>/bin/libEGL.so.1 <- built by this build
<project>/bin/libGLdispatch.so.0 <- built by this build
<project>/bin/libwayland-client.so.0 <- built by this build
<project>/bin/libwayland-server.so.0 <- built by this build
<project>/bin/libffi.so.8 <- built by this build
compat-x-libgbm/25.0.7/…/libgbm.so.1
xim-x-expat/2.6.2/lib/libexpat.so.1
xim-x-gcc/16.1.0/lib64/libgcc_s.so.1
xim-x-gcc/16.1.0/lib64/libstdc++.so.6
xim-x-glibc/2.44/lib64/libc.so.6
xim-x-glibc/2.44/lib64/libm.so.6
```

Nothing is under `/usr/lib` or `/lib64`. Three things there are worth reading
closely.

**The first six lines.** They are this project's own build output, not the
ecosystem's copies — including `libdrm.so.2`, even though Mesa's `libgbm.so.1`
has a DT_NEEDED on that soname and an absolute RUNPATH into the payload. The
consumer links them directly, so they are mapped first, and Mesa binds to them:
the `gbm_bo_create` above ran through this libdrm.

**`libEGL.so.1` and `libGLdispatch.so.0` are on that list.** The ecosystem's
`xim:libglvnd` carries both under the same sonames, and it is installed on this
machine — but only one library per soname is ever mapped and nothing warns
about the loser, so "EGL worked" is not evidence that *this* EGL worked. The
index's test member pins it from the other direction with `dladdr`, and this
example prints `EGL 1.5, vendor Mesa Project` through the build above.

**`libffi` and `libGLdispatch`.** Nothing in `mcpp.toml` names either.
`libffi.so.8` is what `libwayland-client` dispatches protocol messages through,
`libGLdispatch` is what libEGL's vendor dispatch needs — the cascade a directly
linked library pulls behind it, which is exactly what a host `-L/usr/lib`
cannot resolve from inside a private loader.

**Verified with the host present, not absent.** The same program was built and
run from scratch inside `xlings subos use … --sandbox --gpu`, in a synthetic
home where none of this machine's checkouts or caches reach — and where `/usr`
is still the host's, so `/usr/lib/x86_64-linux-gnu/libEGL.so.1`,
`libgbm.so.1`, `libdrm.so.2` and `libwayland-client.so.0` are all present and
reachable. Every graphics library still resolved to the project's own output or
the ecosystem payload. "Nothing from the host" is a claim about what *wins*,
not about what exists.

## The packages

Four are built from source and one binds the ecosystem's Mesa, and the split is
not arbitrary. A library is built from source when upstream ships it as a
**separable unit**; it is bound when it is an internal build target of a project
the ecosystem already owns, where building it would mean forking that project.

| package | | what it gives you |
|---|---|---|
| `compat.libdrm` | source | `drmModeGetResources`, `drmModeAddFB2`, `drmModeSetCrtc` — the KMS side |
| `compat.libgbm` | binds `xim:mesa` | `gbm_create_device`, `gbm_bo_create` — buffers out of a DRM device |
| `freedesktop.egl` | source | `eglGetPlatformDisplay(EGL_PLATFORM_GBM_KHR, …)` — rendering onto them, and `import khronos.egl;` |
| `freedesktop.wayland` | source | `libwayland-client.so.0`, and `import freedesktop.wayland.client;` |
| `freedesktop.wayland-server` | source | `libwayland-server.so.0`, and `import freedesktop.wayland.server;` |

**Four of the five are source builds.** GBM is the only binding, and the
paragraphs below are about why the line falls where it does.

libdrm passes the test — an independent freedesktop project with its own
releases — so it is compiled here, five translation units with no dependencies
at all. GBM fails it: `src/gbm/meson.build` is `link_with: [libloader]`, and
`libloader` wants `idep_mesautil`, roughly 120 TUs of Mesa's internal utility
library for one function. It is also a *loader*, and the backends it dlopens
are Mesa's own, so built apart from Mesa it would have nothing to load.

Wayland passes it too, but needed more than a descriptor: its libraries are
mostly **generated** — `protocol/wayland.xml` describes every interface and
`wayland-scanner` emits ~13,000 lines from it — and the generator is a C program
in the same tree that must be compiled first. That does not fit an inline index
descriptor, so it lives in [mcpplibs/wayland](https://github.com/mcpplibs/wayland),
a fork that patches no upstream file. Client and server are two packages because
they are two distinct SONAMEs and Mesa's `libEGL_mesa` has DT_NEEDED on **both**.

**A payload carrying the same library is not a reason to bind**, which is worth
saying because it looks like one. Mesa's `libgbm.so.1` has a DT_NEEDED on
`libdrm.so.2` and an absolute RUNPATH into the payload's copy — and in this
program that RUNPATH loses. A soname already in the link map is reused, so
ld.so never searches for it again: the `libdrm.so.2` this project linked is
mapped first, exactly one is loaded, and Mesa's GBM allocated the buffer above
through it. That only holds because the package builds a *shared* library with
the canonical soname; merged into the consumer as objects there would be two
copies of libdrm's internal state over one set of file descriptors.

EGL used to be the exception that proved the criterion was being applied
loosely. It was a binding for a duller reason than GBM's — libglvnd IS
separable, but `libEGL.so` also needs its generated dispatch stubs,
`winsys_dispatch` and the whole of `libGLdispatch.so`, so it simply had not
been done. "Not done yet" is not a shape, so it is now a source build out of
[mcpplibs/libglvnd](https://github.com/mcpplibs/libglvnd), and the dispatch
tables upstream generates with ~1000 lines of Python are checked into that fork
and diffed by its CI rather than regenerated during your build.

That package also carries `libGLdispatch.so.0`, as a **sibling workspace member
reached by a path dependency** rather than as a second index entry. GLVND
exists to be the one dispatch point in a process; two index entries would let a
project name both and resolve two instances, and — by the same soname-reuse
rule as above — one would be mapped and the other silently discarded.

One honest gap, since "Mesa/Vulkan" usually get named together: Vulkan is not
part of this example and is not in the same state. `compat.vulkan-runtime`
builds its farm by harvesting the host's ICDs out of `/usr/lib/*` and `/lib64`,
because a Vulkan driver is the GPU vendor's and there is no ecosystem payload
to bind to yet. The GBM/KMS/EGL/Wayland stack above has no such edge.

## Two things worth knowing

**Neither `GBM_BACKENDS_PATH` nor `__EGL_VENDOR_LIBRARY_DIRS` is set by any of
these packages**, and both are needed, because two of the five are *loaders*.
`gbm_create_device()` dlopens `<path>/<driver>_gbm.so`; `eglInitialize()`
dlopens whatever a JSON file in the vendor directory names. The paths upstream
compiles in — `/usr/lib/gbm`, `<prefix>/share/glvnd/egl_vendor.d` — are correct
on a distribution and wrong the moment the payload lives anywhere else.

Those two variables are Mesa's and GLVND's own mechanisms, and setting them is
the *environment's* job, which is where every relocated stack puts it (Valve's
pressure-vessel, Nix, Conda all do exactly this). Here `xim:mesa` declares both
into the SubOS and mcpp carries SubOS declarations into the processes it
launches, so they are simply already set — which is why the program prints them
rather than computing them.

`freedesktop.egl` goes one step further and compiles in an **empty** default
rather than upstream's. A wrong compiled-in path is worse than no path: it
would make a missing declaration load the *host's* driver into a sandboxed
process, silently and successfully. Empty makes the same situation say "no
vendor found".

**The wayland client and server are separate packages**, and this example asks
for both because it creates a `wl_display` on the server side. That is not a
packaging quirk: they are two SONAMEs, Mesa's `libEGL_mesa` carries DT_NEEDED on
each, and mcpp links every library target in a package against all of that
package's sources — so one package cannot emit two libraries with disjoint
contents. A client-only program drops the second line and links only
`libwayland-client.so.0`.

Both also ship a C++23 module wrapper. `import freedesktop.wayland.client;` in place of
`#include <wayland-client.h>` changes nothing else — every exported name is
upstream's, spelled upstream's way — so this file could switch one line at a
time. It uses the headers here because that is what a ported project looks like
on day one.

## Running it

The DRM section needs a GPU. On a machine without one — a container, most CI
runners — the program says so and everything that does not need hardware has
already run:

```
(no DRM node reached EGL — expected without a GPU)
```

To watch the backend loader itself, ask Mesa:

```bash
EGL_LOG_LEVEL=debug mcpp run
```
46 changes: 46 additions & 0 deletions examples/09-graphics-stack/mcpp.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
[indices]
freedesktop = { path = "/home/speak/workspace/github/mcpplibs/mcpp-index" }

[package]
name = "graphics-stack"
version = "0.1.0"
standard = "c++23"

# The whole KMS/DRM stack, from the index. No host paths, no -L/usr/lib.
#
# The versions are the upstream projects' own release numbers, and the shapes
# differ on one criterion: a library is BUILT FROM SOURCE when upstream ships it
# as a separable unit, and BOUND to the ecosystem's payload when it is an
# internal target of a project the ecosystem already owns.
#
# Exactly ONE of the four is a binding, and it is GBM: it is a build target
# inside Mesa (`src/gbm/meson.build` is `link_with: [libloader]`, which wants
# ~120 TUs of Mesa's internal util library for one function), and it is a
# LOADER whose backends are Mesa's own `dri_gbm.so` — built apart from Mesa it
# would have nothing to load. libdrm, EGL and wayland all pass the test and are
# built from source.
[target.'cfg(linux)'.dependencies.compat]
libdrm = "2.4.134" # source-built; the KMS side: modes, CRTCs, framebuffers
libgbm = "25.0.7" # Mesa's GBM: buffer allocation out of a DRM device

# These three are source builds out of forks that add mcpp support and patch no
# upstream file.
#
# wayland is two packages rather than one with an ldflags escape hatch: the
# client and the server are distinct SONAMEs that Mesa's libEGL_mesa needs BOTH
# of, and mcpp links every library target against all of a package's sources.
#
# egl is libglvnd's vendor-neutral dispatch — the piece that makes GBM useful
# for RENDERING rather than only for allocation. It also carries
# `libGLdispatch.so.0`, as a sibling workspace member reached by a path
# dependency rather than as a second index entry, because being the ONE dispatch
# point in a process is what GLVND is for.
#
# All three ship a C++23 module wrapper too. This example uses the headers, and
# for a reason worth knowing: `EGL_PLATFORM_GBM_KHR` and friends are MACROS, and
# no module can export a macro — so `import khronos.egl;` replaces the declarations but
# never the `#include <EGL/eglext.h>` that the constants come from.
[target.'cfg(linux)'.dependencies.freedesktop]
wayland = "1.26.0"
wayland-server = "1.26.0"
egl = "1.7.0"
Loading
Loading