Skip to content

Commit b56db0b

Browse files
committed
test(eui-neo): cover both entry-point shapes with window members
compat.eui-neo's four existing workspace members are all headless by construction, so nothing built upstream's `core/app/glfw_app_main.cpp` (the `app-main` feature — 584 lines of platform code, zero coverage on zero platforms) and nothing exercised a consumer driving EUI itself. Add one member per entry-point shape: * `eui-neo-window` — `app-main` OFF, hand-written main() + render loop. Covers core::window::createWindow(), the OpenGL backend, ScopedRenderBackend and the freetype text path. * `eui-neo-app-main` — `app-main` ON, no main() at all (catch2-main shape), so `main` must come from the package. Both are linux-gated like imgui-window; build+link is the headless test and the real window run is opt-in via MCPP_RUN_WINDOW=1, bounded to 60 frames so it terminates on its own. eui-neo-app-main cannot put that gate in main() — it has none — so it lives in a namespace-scope constructor. Neither member trusts a frame counter: app::update() reaches compose() and the .onFrame() hook without touching the render backend, so both assert rectDraws > 0 and textDraws > 0 from core::render::lastRenderFrameStats(). textDraws is separate because it is the only path needing freetype plus a usable system font. Also sharpen the descriptor comment on `app-main`. mcpp links a dependency's objects eagerly rather than as lazily-selected archive members, so glfw_app_main.o is always in the link — an app-main project may contain no main() of its own at all, including every `mcpp test` TU. Adding one yields `multiple definition of 'main'` from glfw_app_main.cpp:398. The previous wording described the intent but understated the consequence. Verified on mcpp 0.0.109 / linux-x86_64 / gcc 16.1.0, headless and against a real X11 + GL 4.6 display: eui-neo-window ok (60 frames in 0.98s, 120 rect / 120 text draws) eui-neo-app-main ok (60 frames, 118 rect / 118 text draws) The descriptor change is comment-only; `mcpp xpkg parse` and the three lua lints pass, and the existing eui-neo members stay green.
1 parent b9905a9 commit b56db0b

8 files changed

Lines changed: 717 additions & 0 deletions

File tree

‎.agents/docs/2026-07-29-add-eui-neo-plan.md‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,11 @@ translation unit actually references:
186186

187187
The last row is the one that catches a `default`-based regression.
188188

189+
> Superseded in part by `.agents/docs/2026-07-30-eui-neo-window-and-app-main-members.md`:
190+
> every member above is headless, so the backends were verified by symbol reference
191+
> rather than by rendering. `eui-neo-window` and `eui-neo-app-main` now assert
192+
> `rectDraws`/`textDraws` from a real frame under `MCPP_RUN_WINDOW=1`.
193+
189194
## Features
190195

191196
| feature | gates | default |
@@ -203,6 +208,12 @@ the library, for the same reason it is opt-in here: a consumer with its own `mai
203208
not be handed a second one. A real EUI application enables it and supplies only
204209
`app::dslAppConfig()` and `app::compose()`.
205210

211+
> Sharpened later: mcpp links a dependency's objects *eagerly*, so `glfw_app_main.o` is
212+
> always in the link rather than only when `main` is undefined. An `app-main` project may
213+
> therefore contain no `main()` of its own **at all** — including every `mcpp test` TU.
214+
> `tests/examples/eui-neo-app-main` is built around that constraint; see
215+
> `.agents/docs/2026-07-30-eui-neo-window-and-app-main-members.md`.
216+
206217
**`markdown`** is the more interesting one. `components/markdown.h` is header-only and
207218
compiles one of *two* definitions of `detail::parseMarkdownBlocks` depending on
208219
`EUI_HAS_MD4C` — the md4c parser, or a fallback that wraps the entire source in one
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
# Design doc: window + app-main test members for compat.eui-neo
2+
3+
Date: 2026-07-30
4+
5+
Follows `.agents/docs/2026-07-29-add-eui-neo-plan.md`, which landed `compat.eui-neo`
6+
0.5.3 with four workspace members (`eui-neo`, `eui-neo-markdown`, `eui-neo-vulkan`,
7+
`eui-neo-sdl2`).
8+
9+
## Gap
10+
11+
All four existing members are **headless by construction**. They assert on
12+
`eui::json::Document`, `core::platform::` frame flags, `components::detail::`
13+
markdown blocks and — structurally, via `nm` — which backend the dispatch TUs
14+
reference. None of them:
15+
16+
* creates a window,
17+
* initialises the OpenGL render backend,
18+
* enters a render loop, or
19+
* renders text (the freetype path).
20+
21+
Two consequences:
22+
23+
1. `compat.eui-neo` shipped an `app-main` feature that **nothing built**. Upstream's
24+
`core/app/glfw_app_main.cpp` is 584 lines of platform code and was compiled by
25+
zero members on zero platforms.
26+
2. A consumer that drives EUI itself — `core::window::createWindow()` +
27+
`core::render::createRenderBackend()` + `app::update`/`app::render` — was never
28+
exercised, so it was unknown whether that surface is even reachable through the
29+
descriptor's `include_dirs`.
30+
31+
This adds one member per entry-point shape. They are the two shapes an EUI
32+
application can have, and they are mutually exclusive per project (features resolve
33+
per consuming project).
34+
35+
## Members
36+
37+
| member | `app-main` | entry point | new coverage |
38+
|---|---|---|---|
39+
| `eui-neo-window` | off | hand-written `main()` in the test TU | `core::window`, OpenGL backend, `ScopedRenderBackend`, paced loop, freetype text |
40+
| `eui-neo-app-main` | **on** | upstream `glfw_app_main.cpp` | the feature's source compiles; `main` really comes from the package |
41+
42+
Both are `[target.'cfg(linux)']`-gated, matching `imgui-window`, `gui-stack` and the
43+
rest of the X11/GLFW stack in this index. Off-Linux they declare no dependencies and
44+
the test TU compiles to `int main() { return 0; }`.
45+
46+
Both declare `compat.glfw` **directly** rather than leaning on the transitive edge:
47+
each calls GLFW itself (`glfwPollEvents`/`glfwWindowShouldClose` in one,
48+
`glfwSetWindowShouldClose` in the other), because EUI's `core::window` facade
49+
exposes no event-pump entry point.
50+
51+
## Why the run is opt-in
52+
53+
A window needs a display; CI runners have none. Same answer as `imgui-window`:
54+
build + link is the headless test, and `MCPP_RUN_WINDOW=1` opts into the real run.
55+
56+
```
57+
$ mcpp test -p tests/examples/eui-neo-window # headless CI
58+
compat.eui-neo[own main]: linked; window run is opt-in (MCPP_RUN_WINDOW=1)
59+
60+
$ MCPP_RUN_WINDOW=1 mcpp test -p tests/examples/eui-neo-window
61+
compat.eui-neo[own main]: ok (60 frames in 0.98s, 120 rect / 120 text draws)
62+
```
63+
64+
`eui-neo-app-main` cannot put that gate in `main()` — it does not have one. It goes in
65+
a **namespace-scope constructor**, which runs before the package's `main()`:
66+
67+
```cpp
68+
struct WindowRunGate { WindowRunGate() { if (!getenv("MCPP_RUN_WINDOW")) exit(0); } };
69+
const WindowRunGate g_gate;
70+
```
71+
72+
Both runs are bounded by a 60-frame budget so the opt-in path terminates on its own
73+
instead of waiting for a human to close the window.
74+
75+
## Why `app-main` cannot carry tests — and what that pins
76+
77+
mcpp links a dependency's objects **eagerly**, not as lazily-selected archive members.
78+
`glfw_app_main.o` is therefore always in the link, not only when `main` is still
79+
undefined. Verified by adding a `main()` to an `app-main` project:
80+
81+
```
82+
ld: obj/glfw_app_main.o: in function `main':
83+
core/app/glfw_app_main.cpp:398: multiple definition of `main';
84+
obj/probe.o: first defined here
85+
```
86+
87+
So an `app-main` project may not contain **any** TU of its own that defines `main` —
88+
including every `mcpp test` TU. `eui-neo-app-main` is built around that: no `main()`,
89+
per `tests/examples/catch2-main`. This makes the member a two-sided assertion:
90+
91+
* if `app-main` stops contributing its source → `undefined reference to 'main'` here;
92+
* if `app-main` stops being opt-in → `multiple definition of 'main'` in *every other*
93+
member.
94+
95+
The descriptor comment on the feature is updated to say this; the previous wording
96+
("a consumer that has its own main() must not get a second one") describes the intent
97+
but understates the consequence.
98+
99+
## A green test is not a rendered frame
100+
101+
Per the `eui-neo` plan's "the library really is built" section: a frame counter only
102+
proves `app::update()` ran. `app::update()` reaches `compose()` and the `.onFrame()`
103+
hook without touching the render backend at all, so a member counting frames would
104+
pass against a null backend — which is exactly the failure mode the first revision of
105+
this descriptor shipped (`createRenderBackend()` returning null behind a green CI).
106+
107+
Both members therefore assert on `core::render::lastRenderFrameStats()` and require
108+
`rectDraws > 0` **and** `textDraws > 0` over the frame budget. `textDraws` is split out
109+
from `rectDraws` on purpose: text is the only path that needs freetype plus a usable
110+
system font, and "no font at any of upstream's Linux fallback paths" is a different
111+
failure from "dead backend".
112+
113+
## Loop mechanics worth recording
114+
115+
Two non-obvious things the loop in `eui-neo-window` has to get right, both discovered
116+
by running it:
117+
118+
* **Pacing is mandatory.** `OpenGLRenderBackend::initialize()` calls
119+
`glfwSwapInterval(0)`, so `present()` never blocks on vsync. An unpaced loop ran at
120+
~8300 fps and pinned a core; the member paces off `DslAppConfig::fpsValue`.
121+
* **`onFrame` fires twice per rendered frame.** `app::update()` runs the runtime a
122+
second time with `deltaSeconds == 0` to settle the re-compose that `onFrame` itself
123+
requested (`dsl_app_impl.h`, the `composeRequested()` branch). Counting every
124+
invocation reports 2x the real frame rate — gate on the delta.
125+
126+
Neither is a descriptor bug; both are upstream behaviour a consumer must know, and
127+
neither was discoverable from the headless members.
128+
129+
## Verification
130+
131+
Local, mcpp **0.0.109** (matching `validate.yml` `env.MCPP_VERSION`), linux-x86_64,
132+
gcc 16.1.0, X11 display with NVIDIA GL 4.6.
133+
134+
```
135+
headless MCPP_RUN_WINDOW=1
136+
eui-neo-window ok (0.08s) ok (60 frames, 120 rect / 120 text draws)
137+
eui-neo-app-main ok (0.07s) ok (60 frames, 118 rect / 118 text draws)
138+
eui-neo (existing) ok
139+
eui-neo-markdown (existing) ok
140+
```
141+
142+
`mcpp xpkg parse`, `check_mirror_urls.lua`, `check_package_name.lua` and
143+
`check_cross_package_refs.lua` all pass on the touched descriptor. The descriptor
144+
change is comment-only.
145+
146+
## Not done
147+
148+
* `app-main-sdl2` stays uncovered. It is the same shape against `compat.sdl2`, and
149+
covering it means a fifth window member for one `sources` line; worth doing if the
150+
SDL2 window backend gains real users.
151+
* macOS/Windows only get the build+link half, same as `imgui-window`. The opt-in run
152+
has been exercised on linux-x86_64 only.

‎mcpp.toml‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,11 @@ members = [
2020
"tests/examples/curl",
2121
"tests/examples/eigen",
2222
"tests/examples/eui-neo",
23+
"tests/examples/eui-neo-app-main",
2324
"tests/examples/eui-neo-markdown",
2425
"tests/examples/eui-neo-sdl2",
2526
"tests/examples/eui-neo-vulkan",
27+
"tests/examples/eui-neo-window",
2628
"tests/examples/ffmpeg",
2729
"tests/examples/ffmpeg-module",
2830
"tests/examples/fmtlib.fmt",

‎pkgs/e/compat.eui-neo.lua‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,21 @@ package = {
259259
-- `compat.gtest`'s `main` feature is: a consumer that has its own
260260
-- main() must not get a second one. A real EUI application enables
261261
-- this and supplies only app::dslAppConfig() + app::compose().
262+
--
263+
-- Sharper than "must not get a second one": mcpp links a
264+
-- dependency's objects EAGERLY, not as lazily-selected archive
265+
-- members, so `glfw_app_main.o` is always in the link rather than
266+
-- only when `main` is still undefined. Enabling this feature is
267+
-- therefore incompatible with ANY translation unit of the consumer
268+
-- that defines main() — including every `mcpp test` TU, which means
269+
-- an app-main project cannot carry its own tests/. Verified: adding
270+
-- one yields `multiple definition of 'main'` from
271+
-- glfw_app_main.cpp:398. tests/examples/eui-neo-app-main is
272+
-- structured around that constraint (no main() at all, and its
273+
-- opt-in window run is gated in a namespace-scope constructor
274+
-- because there is no main() of ours to gate it in);
275+
-- tests/examples/eui-neo-window is the same UI with the feature OFF
276+
-- and a hand-written loop.
262277
["app-main"] = { sources = { "*/core/app/glfw_app_main.cpp" } },
263278
-- Same gate for the SDL2 window backend. Upstream picks between the
264279
-- two by EUI_APP_MAIN_SOURCE; here the consumer picks by name, and
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# compat.eui-neo, `app-main` FEATURE path: the package supplies `int main()`
2+
# (upstream core/app/glfw_app_main.cpp) and this project deliberately defines
3+
# none. If the feature stops contributing that source, this member fails to link
4+
# with `undefined reference to 'main'` — the same assertion shape as
5+
# tests/examples/catch2-main.
6+
#
7+
# This cannot share a project with tests/examples/eui-neo: features are resolved
8+
# per consuming project, and — because mcpp links a dependency's objects
9+
# EAGERLY rather than as lazily-selected archive members — a project that
10+
# enables `app-main` may not contain any translation unit of its own that
11+
# defines main(). See the note in pkgs/e/compat.eui-neo.lua.
12+
#
13+
# Linux-only (cfg): the whole X11/GLFW/GL stack is linux-gated across this
14+
# index, matching tests/examples/imgui-window. Off-Linux the member has no
15+
# dependencies and the test TU compiles to a no-op.
16+
#
17+
# compat.glfw is a DIRECT dependency because the test TU calls
18+
# glfwSetWindowShouldClose() itself to bound the opt-in window run.
19+
#
20+
# The `compat` index redirect is inherited from the workspace root.
21+
[package]
22+
name = "eui-neo-app-main-tests"
23+
version = "0.1.0"
24+
25+
[target.'cfg(linux)'.dependencies.compat]
26+
eui-neo = { version = "0.5.3", features = ["app-main"] }
27+
glfw = "3.4"

0 commit comments

Comments
 (0)