diff --git a/docs/01-examples.md b/docs/01-examples.md index 6365027a..ce462042 100644 --- a/docs/01-examples.md +++ b/docs/01-examples.md @@ -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.]` 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 diff --git a/docs/zh/01-examples.md b/docs/zh/01-examples.md index e20aca86..f591ef3d 100644 --- a/docs/zh/01-examples.md +++ b/docs/zh/01-examples.md @@ -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.]` 与 `[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` | ## 推荐阅读顺序 diff --git a/examples/09-graphics-stack/README.md b/examples/09-graphics-stack/README.md new file mode 100644 index 00000000..16c33580 --- /dev/null +++ b/examples/09-graphics-stack/README.md @@ -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 ``, +``, `` and `` 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 +``` + +``` +/bin/libdrm.so.2 <- built by this build +/bin/libEGL.so.1 <- built by this build +/bin/libGLdispatch.so.0 <- built by this build +/bin/libwayland-client.so.0 <- built by this build +/bin/libwayland-server.so.0 <- built by this build +/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 `/_gbm.so`; `eglInitialize()` +dlopens whatever a JSON file in the vendor directory names. The paths upstream +compiles in — `/usr/lib/gbm`, `/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 ` 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 +``` diff --git a/examples/09-graphics-stack/mcpp.toml b/examples/09-graphics-stack/mcpp.toml new file mode 100644 index 00000000..479ffb7c --- /dev/null +++ b/examples/09-graphics-stack/mcpp.toml @@ -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 ` that the constants come from. +[target.'cfg(linux)'.dependencies.freedesktop] +wayland = "1.26.0" +wayland-server = "1.26.0" +egl = "1.7.0" diff --git a/examples/09-graphics-stack/src/main.cpp b/examples/09-graphics-stack/src/main.cpp new file mode 100644 index 00000000..18350dbc --- /dev/null +++ b/examples/09-graphics-stack/src/main.cpp @@ -0,0 +1,133 @@ +// System-level graphics with no host dependency. +// +// This is the sequence a KMS/DRM program or a Wayland compositor actually +// runs. Every header here is the stock upstream one and every call is the +// stock upstream API — there is nothing mcpp-specific in this file, which is +// the point: code written against these libraries anywhere else compiles here +// unchanged. + +#include // compat.libgbm +#include // compat.libdrm +#include +#include +#include // freedesktop.egl +#include +#include // compat.wayland +#include + +#include +#include +#include +#include + +#include +#include + +namespace { + +// The two APIs exchange this value across the gbm_bo -> drmModeAddFB2 +// boundary. If the packages ever disagreed about it, a display would show the +// wrong colours and nothing would report an error — so it is worth asserting +// rather than assuming. +static_assert(GBM_FORMAT_XRGB8888 == DRM_FORMAT_XRGB8888, + "libgbm and libdrm must agree on the XRGB8888 fourcc"); + +void report(const char *label, const void *p) +{ + std::printf(" %-24s %p\n", label, p); +} + +} // namespace + +int main() +{ + std::puts("== the graphics stack, resolved from the index =="); + + // Where the two LOADERS in this stack find what they dlopen. Nothing in + // this program and none of the packages sets either: `xim:mesa` declares + // both into the SubOS through the graphics discovery layer, and mcpp + // carries SubOS declarations into the processes it launches. + // + // GBM_BACKENDS_PATH -> gbm_create_device() dlopens /_gbm.so + // __EGL_VENDOR_LIBRARY_DIRS -> eglInitialize() dlopens what a JSON there names + for (const char *name : {"GBM_BACKENDS_PATH", "__EGL_VENDOR_LIBRARY_DIRS"}) { + const char *value = std::getenv(name); + std::printf(" %-25s = %s\n", name, + value ? value : ""); + } + + // Wayland: build a server-side display. No socket is bound, so this needs + // no session and no privileges — the cheapest proof the library is live. + if (wl_display *server = wl_display_create()) { + report("wl_display_create", server); + wl_display_destroy(server); + } else { + std::puts(" wl_display_create FAILED"); + return 1; + } + + // The real chain: a DRM node becomes a GBM device, which becomes an EGL + // display. This is what "headless GPU rendering" means concretely, and it + // is the sequence that cannot be expressed without all three packages. + std::puts("-- DRM node -> GBM device -> EGL display --"); + bool reached_egl = false; + + for (const char *node : {"/dev/dri/renderD128", "/dev/dri/card0"}) { + const int fd = ::open(node, O_RDWR); + if (fd < 0) { + std::printf(" %-24s (not present on this machine)\n", node); + continue; + } + std::printf(" %s\n", node); + + if (drmVersionPtr v = drmGetVersion(fd)) { + std::printf(" %-24s %s\n", "drm driver", v->name); + drmFreeVersion(v); + } + + if (gbm_device *gbm = gbm_create_device(fd)) { + report("gbm_create_device", gbm); + + // Actually allocate GPU memory. Creating the device only proves the + // backend loaded; a buffer object is the thing a compositor hands + // to drmModeAddFB2 for scanout, and its stride and modifier come + // back from the driver rather than from libgbm. + if (gbm_bo *bo = gbm_bo_create(gbm, 256, 256, GBM_FORMAT_XRGB8888, + GBM_BO_USE_RENDERING)) { + std::printf(" %-24s 256x256 stride=%u modifier=0x%llx\n", + "gbm_bo_create", gbm_bo_get_stride(bo), + (unsigned long long)gbm_bo_get_modifier(bo)); + gbm_bo_destroy(bo); + } else { + std::printf(" %-24s (driver declined this format/usage)\n", + "gbm_bo_create"); + } + + EGLDisplay dpy = + eglGetPlatformDisplay(EGL_PLATFORM_GBM_KHR, gbm, nullptr); + report("eglGetPlatformDisplay", dpy); + + if (dpy != EGL_NO_DISPLAY) { + EGLint major = 0, minor = 0; + if (eglInitialize(dpy, &major, &minor)) { + std::printf(" %-24s EGL %d.%d, vendor %s\n", "eglInitialize", + major, minor, eglQueryString(dpy, EGL_VENDOR)); + reached_egl = true; + eglTerminate(dpy); + } + } + gbm_device_destroy(gbm); + } + ::close(fd); + } + + if (!reached_egl) { + // Not a failure of the packages: a machine with no DRM node (a + // container, most CI runners) legitimately gets here. Everything above + // that does not need hardware has already run. + std::puts(" (no DRM node reached EGL — expected without a GPU)"); + } + + std::puts("done."); + return 0; +}