Skip to content

Commit 4830645

Browse files
feat: add compat.vulkan/sdl2/curl + glx-headers, wire eui-neo's alternate backends
compat.eui-neo shipped one configuration: OpenGL, GLFW, no HTTP. The three upstream alternatives were left out because the index had no package behind them. This adds five packages and makes the choice real. compat.vulkan-headers 1.4.357.0 Khronos headers compat.vulkan 1.4.357.0 Khronos loader, from source compat.curl 8.21.0 libcurl, OpenSSL / Schannel TLS compat.sdl2 2.32.10 SDL2 compat.glx-headers 1.7.0 libglvnd's GL/glx.h All five build from plain source lists — no CMake, no autotools, no install() hook. The Vulkan loader manages it because loader/generated/ is checked in and the GAS/MASM path is optional (UNKNOWN_FUNCTIONS_SUPPORTED gates it; upstream itself falls back to a pure-C implementation). curl and SDL manage it because both compile every unselected backend to an empty TU, so configuration lives entirely in a generated config header — needed only where upstream has a gap: unix for curl, linux for SDL, since both check in configs for the rest. compat.glx-headers fills a real hole: GL/glx.h is not in the Khronos OpenGL-Registry, so compat.opengl cannot supply it, and SDL's X11 driver does not build without it. Backend exclusivity was the hard part, and three encodings failed SILENTLY before one worked. On mcpp 0.0.109 a `default` feature carrying defines/sources/deps is inert — not "suppressed when features are named", never applied at all — while `default = { implies = ... }` is the opposite and always applies. Both were verified with probes. The first one is dangerous: an eui-neo member passed its smoke test while createRenderBackend() compiled to its #else branch and returned a null backend, because a headless test never asks for one. What works is resolving the choice in the preprocessor from the -DMCPP_FEATURE_<NAME> flags mcpp already passes, via a force-included header. That in turn exposed a bug already present in the parent commit: mcpp routes `cflags` to C translation units and `cxxflags` to C++ ones, so `cflags = { "-DEUI_RENDER_BACKEND_OPENGL=1" }` reached only the three .c bridge files and never render_backend.cpp. The package it shipped had no render backend at all. Both lists now carry the flag; NOMINMAX got the same fix. Verified cold on linux-x86_64 with mcpp 0.0.109. SDL2 is the one backend that genuinely runs headless — its dummy video driver executes SDL_Init, SDL_CreateWindow and a full event round-trip. Vulkan and curl are asserted on what is answerable without a GPU or a network. Backend selection is verified structurally rather than by trusting a green test: render_backend.o and window_backend.o are checked for which backend they actually reference, across all four members including the one that names an unrelated feature. CN mirrors published to gitcode mcpp-res for all five, byte-identical. Design: .agents/docs/2026-07-29-eui-neo-backends-plan.md Co-authored-by: SPeak Agent <248744407+speak-agent@users.noreply.github.com>
1 parent ff226c0 commit 4830645

20 files changed

Lines changed: 2511 additions & 12 deletions

File tree

Lines changed: 277 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,277 @@
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.

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,10 @@ mcpp self config --mirror CN # 切换至国内镜像,默认使用 GLOBAL 上
3535
| 原生模块库(Form A) | [`mcpplibs.xpkg`](pkgs/x/xpkg.lua) · [`mcpplibs.tinyhttps`](pkgs/t/tinyhttps.lua) · [`tensorvia-cpu`](pkgs/t/tensorvia-cpu.lua) · [`ffmpeg`](pkgs/f/ffmpeg.lua)(模块层,源码经 `compat.ffmpeg` 直编) · [`opencv`](pkgs/o/opencv.lua)(单仓库:模块层与 OpenCV 5 全源码构建同在包内,索引侧只留本描述符) |
3636
| C 源码 compat(含 `features`) | [`compat.cjson`](pkgs/c/compat.cjson.lua) · [`compat.zlib`](pkgs/c/compat.zlib.lua) |
3737
| header-only(含 `features`) | [`compat.eigen`](pkgs/c/compat.eigen.lua) |
38+
| 互斥 feature(同包多后端二选一) | [`compat.eui-neo`](pkgs/e/compat.eui-neo.lua)`opengl`/`vulkan``glfw`/`sdl2`**`default` feature 在 mcpp 0.0.109 上不可用**(带 `defines/sources/deps` 时完全不生效;带 `implies` 时反而恒生效),可行解是用 `-DMCPP_FEATURE_<NAME>` 在强制包含头里做前置判定。另注意 `cflags` 只作用于 C TU,C++ 需 `cxxflags`。详见 `.agents/docs/2026-07-29-eui-neo-backends-plan.md` |
39+
| 运行时 loader compat(纯源码,绕开上游 codegen/asm) | [`compat.vulkan`](pkgs/c/compat.vulkan.lua) · [`compat.vulkan-headers`](pkgs/c/compat.vulkan-headers.lua) |
40+
| 全源码直编 + 生成 config(仅缺口平台) | [`compat.curl`](pkgs/c/compat.curl.lua)(win32 用上游签入 config,unix 生成) · [`compat.sdl2`](pkgs/c/compat.sdl2.lua)(win/mac 用上游签入 config,linux 生成 + 手工开 X11) |
41+
| 补索引空缺的头文件包 | [`compat.glx-headers`](pkgs/c/compat.glx-headers.lua)(libglvnd 的 `GL/glx.h`,Khronos registry 不含,SDL 的 X11 后端必需) |
3842
| C++ 应用框架 compat(依赖复用索引内既有包) | [`compat.eui-neo`](pkgs/e/compat.eui-neo.lua)(上游 `3rd/` 自带 8 个 vendored 依赖,此处一个不编,全部改指索引内同版本 `compat.*`;`app-main` 门控上游 `main()`,`markdown` 经 interface define 打开消费侧 header) |
3943
| 单包多 major(形态随版本切换) | [`compat.catch2`](pkgs/c/compat.catch2.lua)(3.x 编 `src/catch2/` 出静态库;2.x 走 `single_include/` header-only) |
4044
| 外部构建系统(`install()` 从源码构建) | [`compat.openblas`](pkgs/c/compat.openblas.lua)(Make) · [`compat.openssl`](pkgs/c/compat.openssl.lua)(Perl Configure + Make,静态 libssl/libcrypto) |

mcpp.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,12 @@ members = [
1717
"tests/examples/catch2-v2-main",
1818
"tests/examples/cjson",
1919
"tests/examples/core",
20+
"tests/examples/curl",
2021
"tests/examples/eigen",
2122
"tests/examples/eui-neo",
2223
"tests/examples/eui-neo-markdown",
24+
"tests/examples/eui-neo-sdl2",
25+
"tests/examples/eui-neo-vulkan",
2326
"tests/examples/ffmpeg",
2427
"tests/examples/ffmpeg-module",
2528
"tests/examples/fmtlib.fmt",
@@ -34,13 +37,15 @@ members = [
3437
"tests/examples/opencv-module",
3538
"tests/examples/opencv-module-dnn",
3639
"tests/examples/opencv-module-unifont",
40+
"tests/examples/sdl2",
3741
"tests/examples/spdlog",
3842
"tests/examples/freetype",
3943
"tests/examples/glad",
4044
"tests/examples/libpng",
4145
"tests/examples/md4c",
4246
"tests/examples/spdlog-compiled",
4347
"tests/examples/tinyhttps",
48+
"tests/examples/vulkan",
4449
"tests/examples/tray",
4550
"tests/examples/yyjson",
4651
]

0 commit comments

Comments
 (0)