|
| 1 | +# Design doc: eui-neo alternate backends (`vulkan`, `sdl2`, `network`) |
| 2 | + |
| 3 | +Date: 2026-07-29 · Stacked on #133 (`compat.eui-neo` 0.5.3) |
| 4 | + |
| 5 | +`compat.eui-neo` as merged in #133 models exactly one configuration: OpenGL render |
| 6 | +backend, GLFW window backend, no HTTP. The three upstream alternatives were left out |
| 7 | +because the index had no package behind them. This adds those packages and wires the |
| 8 | +features. |
| 9 | + |
| 10 | +| feature | needs | status | |
| 11 | +|---|---|---| |
| 12 | +| `vulkan` | `compat.vulkan` + `compat.vulkan-headers` | done | |
| 13 | +| `sdl2` | `compat.sdl2` (+ `compat.glx-headers`) | done | |
| 14 | +| `network` | `compat.curl` | done | |
| 15 | + |
| 16 | +## The hard part was not the packages |
| 17 | + |
| 18 | +Upstream selects both backends at configure time and compiles exactly one of each: |
| 19 | +`core/render/render_backend.cpp` dispatches on |
| 20 | +`#if defined(EUI_RENDER_BACKEND_OPENGL) … #elif defined(…VULKAN)`, and |
| 21 | +`core/window/window_backend.cpp` on `#if defined(EUI_WINDOW_BACKEND_SDL2)` / else-GLFW. |
| 22 | +Define both halves of either pair and the first silently wins — the caller's choice is |
| 23 | +ignored, and since neither backend runs headless, CI would never notice. |
| 24 | + |
| 25 | +mcpp features are additive and there is no `default-features = false` (mcpp#242 — the same |
| 26 | +wall that stopped the ffmpeg trimmed profiles, see |
| 27 | +`2026-07-19-compat-ffmpeg-trimmed-profiles-decision.md`). Three encodings were tried, and |
| 28 | +**each failed silently**, which is why each is written down here: |
| 29 | + |
| 30 | +| encoding | behaviour on 0.0.109 | |
| 31 | +|---|---| |
| 32 | +| `default = { defines/sources/deps = … }` | **inert** — never applied at all | |
| 33 | +| `default = { implies = { … } }` | **always** applied, even when the consumer names a different feature | |
| 34 | +| package-level define + additive feature | feature cannot unset the define | |
| 35 | + |
| 36 | +The middle row is what makes the first row so dangerous: an early revision here read |
| 37 | +"`default` is suppressed when features are named", which is Cargo's rule and looks |
| 38 | +identical to "never applied" from a single observation. The build that convinced me |
| 39 | +otherwise was the plain `eui-neo` member — it passed its smoke test while |
| 40 | +`createRenderBackend()` compiled to its `#else` branch and returned a null backend, |
| 41 | +because nothing in a headless test ever asks for one. |
| 42 | + |
| 43 | +### What actually works |
| 44 | + |
| 45 | +mcpp passes `-DMCPP_FEATURE_<NAME>` for every enabled feature into the package's own |
| 46 | +translation units. So the exclusivity is resolved in the preprocessor, by a force-included |
| 47 | +generated header, and the features carry only sources and dependencies: |
| 48 | + |
| 49 | +```c |
| 50 | +#if defined(MCPP_FEATURE_VULKAN) |
| 51 | +# define EUI_RENDER_BACKEND_VULKAN 1 |
| 52 | +#else |
| 53 | +# define EUI_RENDER_BACKEND_OPENGL 1 |
| 54 | +#endif |
| 55 | +#if defined(MCPP_FEATURE_SDL2) |
| 56 | +# define EUI_WINDOW_BACKEND_SDL2 1 |
| 57 | +#endif |
| 58 | +``` |
| 59 | + |
| 60 | +This is strictly better than anything built on `default`: naming an unrelated feature no |
| 61 | +longer drops the backends. |
| 62 | + |
| 63 | +```toml |
| 64 | +eui-neo = "0.5.3" # opengl + glfw |
| 65 | +eui-neo = { …, features = ["vulkan"] } # vulkan + glfw |
| 66 | +eui-neo = { …, features = ["sdl2"] } # opengl + SDL2 |
| 67 | +eui-neo = { …, features = ["vulkan","sdl2"] } # vulkan + SDL2 |
| 68 | +eui-neo = { …, features = ["markdown"] } # opengl + glfw, markdown on |
| 69 | +``` |
| 70 | + |
| 71 | +### `cflags` is C-only — and #133 was already wrong about it |
| 72 | + |
| 73 | +Getting the header force-included exposed a second, larger problem: **mcpp routes `cflags` |
| 74 | +to C translation units and `cxxflags` to C++ ones.** With `-include` in `cflags` alone, |
| 75 | +exactly three objects received it — `ime_bridge.c`, `native_bridge.c`, `tray_bridge.c` — |
| 76 | +and every `.cpp` in the package compiled without it. |
| 77 | + |
| 78 | +That is not specific to this change. **#133 as it stands carries |
| 79 | +`cflags = { "-DEUI_RENDER_BACKEND_OPENGL=1" }` and nothing else**, so its |
| 80 | +`render_backend.cpp` has never seen the define either: the package it ships builds, links, |
| 81 | +passes its tests, and has no render backend at all. This PR fixes it, and the fix belongs |
| 82 | +to #133 as much as to this one. |
| 83 | + |
| 84 | +Both lists now carry the flag. `NOMINMAX` on Windows got the same treatment for the same |
| 85 | +reason. |
| 86 | + |
| 87 | +## `compat.vulkan-headers` + `compat.vulkan` |
| 88 | + |
| 89 | +Split along upstream's own repository split. Headers are header-only (the `compat.opengl` |
| 90 | +shape: include root plus an anchor TU). The loader is what a Vulkan program actually |
| 91 | +links — `vkCreateInstance` and friends are loader trampolines that dispatch into whatever |
| 92 | +ICD the system advertises. |
| 93 | + |
| 94 | +**The loader builds as a plain source list — no CMake, no Python, no assembler.** Two |
| 95 | +things make that true: |
| 96 | + |
| 97 | +- `loader/generated/` (`vk_loader_extensions.c`, `vk_object_types.h`, …) is checked in |
| 98 | + upstream, so CMake's codegen step is unnecessary. |
| 99 | +- The assembly path is optional. Upstream compiles `dev_ext_trampoline.c` + |
| 100 | + `phys_dev_ext.c` against hand-written GAS/MASM plus a `gen_defines.asm` that requires |
| 101 | + building *and running* `asm_offset`, then scraping its output with a Python script. |
| 102 | + That whole chain is gated on `UNKNOWN_FUNCTIONS_SUPPORTED`; upstream itself degrades |
| 103 | + when no assembler is found and `unknown_function_handling.c` compiles a pure-C |
| 104 | + fallback. We take the fallback deliberately. The cost is that unknown *device* |
| 105 | + extension entry points get no trampoline — nothing in this index uses them. |
| 106 | + |
| 107 | +Two non-obvious requirements found by building it: |
| 108 | + |
| 109 | +- `VK_ENABLE_BETA_EXTENSIONS` is mandatory despite the name. The checked-in |
| 110 | + `generated/vk_object_types.h` references `VK_OBJECT_TYPE_CUDA_MODULE_NV`, which the |
| 111 | + headers only declare under that macro. Without it the loader does not compile at all. |
| 112 | +- `SYSCONFDIR` / `FALLBACK_*_DIRS` must arrive as string literals, and |
| 113 | + `-DSYSCONFDIR="/etc"` does not survive mcpp's flag splitting (mcpp#234): `loader.c` |
| 114 | + sees a bare `/etc` and fails with *expected expression before '/' token*. They are |
| 115 | + carried in a force-included generated header instead. |
| 116 | + |
| 117 | +WSI is enabled per platform — Xlib + XCB on Linux (which makes `compat.x11` / `compat.xcb` |
| 118 | +/ `compat.xorgproto` a **compile** dependency of the loader, since `vulkan_xlib.h` includes |
| 119 | +`<X11/Xlib.h>`), Win32 on Windows, Metal on macOS. Search paths are the FHS/XDG defaults |
| 120 | +because this package must find the *host's* ICDs. |
| 121 | + |
| 122 | +## Platform-specific things only CI could find |
| 123 | + |
| 124 | +Linux passed everything on the first try; macOS and Windows each surfaced a distinct |
| 125 | +class of problem, and one of them was a repeat of a trap this repo has already documented. |
| 126 | + |
| 127 | +- **SDL2's tag archive cannot be extracted on Windows.** It carries two POSIX symlinks |
| 128 | + under `android-project-ant/`, and the package then "installs" empty: it reports success, |
| 129 | + compiles nothing, publishes no include dirs, and every consumer fails with |
| 130 | + `'SDL.h' file not found` — the package itself never errors. |
| 131 | + `chriskohlhoff.asio` hit this and set the precedent, so GLOBAL now points at a |
| 132 | + symlink-free repack on `xlings-res/sdl2` with the identical bytes mirrored to |
| 133 | + `mcpp-res` for CN. Only the two symlink entries are removed. |
| 134 | +- **SDL on Apple requires ARC.** Its cocoa classes declare |
| 135 | + `__weak SDL_WindowData *_data`, and a `__weak` ivar without `-fobjc-arc` is rejected |
| 136 | + outright (`'_data' is unavailable`). Upstream's CMake hard-fails when the compiler |
| 137 | + cannot do ARC, for this reason. |
| 138 | +- **curl needs to know which `strerror_r` it has** and refuses to build otherwise |
| 139 | + ("strerror_r MUST be either POSIX, glibc style"). Apple ships the POSIX one; glibc's |
| 140 | + returns `char*`. Only the linux branch had it. |
| 141 | +- **curl on Windows needs `secur32`** for `InitSecurityInterfaceA`, which `curl_sspi.c` |
| 142 | + uses for the SSPI auth that Schannel builds on. |
| 143 | +- **`src/thread/generic` and `src/thread/windows` share basenames** (`SDL_syscond.c`, |
| 144 | + `SDL_sysmutex.c`, …). Listing both would have them silently displace each other in |
| 145 | + mcpp's flat per-link object directory — the mcpp#233 collision again, now avoided by |
| 146 | + listing only the platform's own set, which is what upstream does anyway. |
| 147 | + |
| 148 | +- **A static Vulkan loader is not supported on Windows.** Upstream's only static option is |
| 149 | + `APPLE_STATIC_LOADER`, gated to macOS and carrying the warning that it "will only work on |
| 150 | + MacOS and is not supported" elsewhere. Built anyway, the Windows loader links and then |
| 151 | + faults at the first entry point (0xC0000005 out of `vkEnumerateInstanceVersion`). Linux |
| 152 | + is not covered by that option either, but a static loader is the ordinary case there and |
| 153 | + works. So `compat.vulkan` declares no windows xpm entry, following `compat.openssl`, and |
| 154 | + the two Vulkan members gate with `[target.'cfg(...)']` — on windows they assert the |
| 155 | + default OpenGL configuration instead of skipping outright. |
| 156 | +- **macOS needs `APPLE_STATIC_LOADER`, both WSI platform macros, and CoreFoundation.** |
| 157 | + `wsi.c` `#error`s with "VK_USE_PLATFORM_MACOS_MVK not defined!" when only |
| 158 | + `VK_USE_PLATFORM_METAL_EXT` is set, and the loader reads bundle paths through CF. |
| 159 | + |
| 160 | +## Verification |
| 161 | + |
| 162 | +Local, linux-x86_64, mcpp 0.0.109 (the `validate.yml` pin), gcc 16.1.0. Every member cold. |
| 163 | + |
| 164 | +``` |
| 165 | +$ mcpp test -p vulkan |
| 166 | +compat.vulkan: ok (loader api 1.4.357, 4 loader extension(s), WSI trampolines linked) |
| 167 | +$ mcpp test -p curl |
| 168 | +compat.curl: ok (8.21.0-DEV, ssl=OpenSSL/3.5.1, http+https present) |
| 169 | +$ mcpp test -p sdl2 |
| 170 | +compat.sdl2: ok (SDL 2.32.10, driver=dummy, 320x240 window, events round-tripped) |
| 171 | +
|
| 172 | +$ mcpp test -p eui-neo ok |
| 173 | +$ mcpp test -p eui-neo-markdown ok |
| 174 | +$ mcpp test -p eui-neo-vulkan ok |
| 175 | +$ mcpp test -p eui-neo-sdl2 ok |
| 176 | +``` |
| 177 | + |
| 178 | +### Backend selection, verified structurally |
| 179 | + |
| 180 | +A passing test proves nothing here — that is the whole lesson above. What is checked is |
| 181 | +which backend each member's dispatch translation unit actually references: |
| 182 | + |
| 183 | +| member | `render_backend.o` → | `window_backend.o` → | |
| 184 | +|---|---|---| |
| 185 | +| `eui-neo` (no features) | `OpenGLRenderBackend` | `glfwCreateWindow` | |
| 186 | +| `eui-neo-vulkan` | `VulkanRenderBackend` | `glfwCreateWindow` | |
| 187 | +| `eui-neo-sdl2` | `OpenGLRenderBackend` | `SDL_CreateWindow` | |
| 188 | +| `eui-neo-markdown` | `OpenGLRenderBackend` | `glfwCreateWindow` | |
| 189 | + |
| 190 | +All four combinations resolve correctly, and the last row is the one that catches a |
| 191 | +`default`-based regression: an unrelated feature must not cost you the backends. |
| 192 | + |
| 193 | +### What the tests can and cannot assert |
| 194 | + |
| 195 | +- **SDL2 genuinely runs.** Its `dummy` video driver is a real driver, so |
| 196 | + `tests/examples/sdl2` and `eui-neo-sdl2` execute `SDL_Init`, `SDL_CreateWindow`, |
| 197 | + `SDL_GetWindowSize` and a full event round-trip. This is the only backend in the family |
| 198 | + that can be exercised rather than merely linked. |
| 199 | +- **Vulkan is driverless by construction.** The loader advertises WSI *surface* extensions |
| 200 | + only when an ICD supports them, so `VK_KHR_surface` is legitimately absent on a runner |
| 201 | + with no GPU — an early draft asserted on it and failed, which is exactly the |
| 202 | + "testing the runner's hardware" trap. Asserted instead: `vkEnumerateInstanceVersion` |
| 203 | + (answerable only by the loader), `VK_EXT_debug_utils` (loader-implemented, so |
| 204 | + driver-independent), and a reference to `vkDestroySurfaceKHR` as link-time proof that |
| 205 | + `wsi.c` is in the library. |
| 206 | +- **curl never opens a connection.** Asserted: init succeeds, `CURL_VERSION_SSL` with a |
| 207 | + non-null `ssl_version` (a curl built without TLS still links and runs — it just cannot |
| 208 | + do https, which is the failure EUI-NEO's `network` feature would hit at runtime), |
| 209 | + http+https in the protocol list, and that a handle accepts the exact options |
| 210 | + `core/platform/network.cpp` sets. |
| 211 | +- **No frame is drawn anywhere**, on any platform. Device selection, swapchain, GL context |
| 212 | + creation and rendering are all unexercised. |
| 213 | + |
| 214 | +## CN mirrors |
| 215 | + |
| 216 | +All five new packages published to gitcode `mcpp-res`, byte-identical to GLOBAL, `curl` |
| 217 | +returns 200 for each: |
| 218 | + |
| 219 | +``` |
| 220 | +mcpp-res/vulkan-headers 1.4.357.0 |
| 221 | +mcpp-res/vulkan 1.4.357.0 |
| 222 | +mcpp-res/curl 8.21.0 |
| 223 | +mcpp-res/sdl2 2.32.10 |
| 224 | +mcpp-res/glx-headers 1.7.0 |
| 225 | +``` |
| 226 | + |
| 227 | +## `compat.curl` |
| 228 | + |
| 229 | +Full-source direct build rather than an `install()` hook, which works because curl |
| 230 | +compiles every unselected protocol and TLS backend to an EMPTY translation unit |
| 231 | +(`vtls/gtls.c` is `#ifdef USE_GNUTLS` end to end). The source list is plain globs; only |
| 232 | +`lib/dllmain.c` is excluded. |
| 233 | + |
| 234 | +The config header is the awkward part, and only on unix: `lib/config-win32.h` is checked |
| 235 | +in and `curl_setup.h` picks it automatically when `HAVE_CONFIG_H` is absent, so Windows |
| 236 | +needs nothing generated. linux/macosx get a generated `curl_config.h` branching on |
| 237 | +`__linux__` / `__APPLE__`, with the Apple branch deliberately the conservative subset — |
| 238 | +an omitted `HAVE_*` costs curl a fallback path, never correctness. |
| 239 | + |
| 240 | +Generating it against the *right* compiler mattered: a first pass with the host `cc` |
| 241 | +produced a config asserting `ssize_t` did not exist (its probe failed for an unrelated |
| 242 | +sysroot reason), and curl then failed to compile against its own config. |
| 243 | + |
| 244 | +TLS is OpenSSL on linux/macosx via the existing `compat.openssl`, and Schannel on Windows |
| 245 | +— which is built into the OS, and is necessary because `compat.openssl` has no Windows |
| 246 | +build. |
| 247 | + |
| 248 | +`CURL_STATICLIB` has to reach consumers (`<curl/curl.h>` marks its declarations |
| 249 | +`__declspec(dllimport)` otherwise). mcpp has no package-level interface `defines`, but a |
| 250 | +feature's `defines` are interface-visible, so it rides on a `default` feature — safe here |
| 251 | +precisely because this package has no other feature to name. |
| 252 | + |
| 253 | +## `compat.sdl2` and `compat.glx-headers` |
| 254 | + |
| 255 | +SDL is the mirror image of curl: upstream **checks in** `SDL_config_windows.h` and |
| 256 | +`SDL_config_macosx.h` and dispatches to them from `include/SDL_config.h`; only Linux falls |
| 257 | +through to `SDL_config_minimal.h`, which builds an SDL that can do essentially nothing. So |
| 258 | +the generated header reproduces upstream's dispatch for windows/macosx and carries CMake's |
| 259 | +Linux output inline. |
| 260 | + |
| 261 | +Three things the Linux config needed by hand, each found by building: |
| 262 | + |
| 263 | +- **X11 switched on.** The CMake probe ran on a host without system X11 development |
| 264 | + headers and disabled it, leaving only dummy/offscreen. This index does supply X11. |
| 265 | + Non-dynamic, because `compat.x11` is static. |
| 266 | +- **libudev off.** A host library this index does not package; SDL falls back to scanning |
| 267 | + `/dev/input`, as it does on any udev-less system. |
| 268 | +- **`src/core/linux` listed file by file.** The one directory where a glob over-reaches: |
| 269 | + `SDL_dbus.c` / `SDL_ibus.c` / `SDL_fcitx.c` / `SDL_ime.c` are *not* internally guarded |
| 270 | + and simply fail without dbus-1 headers. |
| 271 | + |
| 272 | +`compat.glx-headers` exists because SDL's X11 driver includes `<GL/glx.h>`, and **no |
| 273 | +package in this index had it** — `GL/glx.h` is not part of the Khronos OpenGL-Registry, so |
| 274 | +`compat.opengl` cannot supply it. libglvnd is the canonical provider and is already named |
| 275 | +as a wanted package in `2026-06-03-gl-runtime-packages-plan.md`. Only its headers are |
| 276 | +taken; `compat.glx-runtime` still owns the runtime side. It overlaps `compat.opengl` on |
| 277 | +`GL/gl.h`, so consumers should depend on one or the other, never both. |
0 commit comments