From b56db0b10db73ebdaaf3f52884af38da06caf186 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Thu, 30 Jul 2026 08:05:42 +0800 Subject: [PATCH 1/4] test(eui-neo): cover both entry-point shapes with window members MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .agents/docs/2026-07-29-add-eui-neo-plan.md | 11 + ...-30-eui-neo-window-and-app-main-members.md | 152 +++++++++ mcpp.toml | 2 + pkgs/e/compat.eui-neo.lua | 15 + tests/examples/eui-neo-app-main/mcpp.toml | 27 ++ .../eui-neo-app-main/tests/app_main.cpp | 172 ++++++++++ tests/examples/eui-neo-window/mcpp.toml | 26 ++ .../examples/eui-neo-window/tests/window.cpp | 312 ++++++++++++++++++ 8 files changed, 717 insertions(+) create mode 100644 .agents/docs/2026-07-30-eui-neo-window-and-app-main-members.md create mode 100644 tests/examples/eui-neo-app-main/mcpp.toml create mode 100644 tests/examples/eui-neo-app-main/tests/app_main.cpp create mode 100644 tests/examples/eui-neo-window/mcpp.toml create mode 100644 tests/examples/eui-neo-window/tests/window.cpp diff --git a/.agents/docs/2026-07-29-add-eui-neo-plan.md b/.agents/docs/2026-07-29-add-eui-neo-plan.md index a5abcfd..803043a 100644 --- a/.agents/docs/2026-07-29-add-eui-neo-plan.md +++ b/.agents/docs/2026-07-29-add-eui-neo-plan.md @@ -186,6 +186,11 @@ translation unit actually references: The last row is the one that catches a `default`-based regression. +> Superseded in part by `.agents/docs/2026-07-30-eui-neo-window-and-app-main-members.md`: +> every member above is headless, so the backends were verified by symbol reference +> rather than by rendering. `eui-neo-window` and `eui-neo-app-main` now assert +> `rectDraws`/`textDraws` from a real frame under `MCPP_RUN_WINDOW=1`. + ## Features | feature | gates | default | @@ -203,6 +208,12 @@ the library, for the same reason it is opt-in here: a consumer with its own `mai not be handed a second one. A real EUI application enables it and supplies only `app::dslAppConfig()` and `app::compose()`. +> Sharpened later: mcpp links a dependency's objects *eagerly*, so `glfw_app_main.o` is +> always in the link rather than only when `main` is undefined. An `app-main` project may +> therefore contain no `main()` of its own **at all** — including every `mcpp test` TU. +> `tests/examples/eui-neo-app-main` is built around that constraint; see +> `.agents/docs/2026-07-30-eui-neo-window-and-app-main-members.md`. + **`markdown`** is the more interesting one. `components/markdown.h` is header-only and compiles one of *two* definitions of `detail::parseMarkdownBlocks` depending on `EUI_HAS_MD4C` — the md4c parser, or a fallback that wraps the entire source in one diff --git a/.agents/docs/2026-07-30-eui-neo-window-and-app-main-members.md b/.agents/docs/2026-07-30-eui-neo-window-and-app-main-members.md new file mode 100644 index 0000000..a73ebf8 --- /dev/null +++ b/.agents/docs/2026-07-30-eui-neo-window-and-app-main-members.md @@ -0,0 +1,152 @@ +# Design doc: window + app-main test members for compat.eui-neo + +Date: 2026-07-30 + +Follows `.agents/docs/2026-07-29-add-eui-neo-plan.md`, which landed `compat.eui-neo` +0.5.3 with four workspace members (`eui-neo`, `eui-neo-markdown`, `eui-neo-vulkan`, +`eui-neo-sdl2`). + +## Gap + +All four existing members are **headless by construction**. They assert on +`eui::json::Document`, `core::platform::` frame flags, `components::detail::` +markdown blocks and — structurally, via `nm` — which backend the dispatch TUs +reference. None of them: + +* creates a window, +* initialises the OpenGL render backend, +* enters a render loop, or +* renders text (the freetype path). + +Two consequences: + +1. `compat.eui-neo` shipped an `app-main` feature that **nothing built**. Upstream's + `core/app/glfw_app_main.cpp` is 584 lines of platform code and was compiled by + zero members on zero platforms. +2. A consumer that drives EUI itself — `core::window::createWindow()` + + `core::render::createRenderBackend()` + `app::update`/`app::render` — was never + exercised, so it was unknown whether that surface is even reachable through the + descriptor's `include_dirs`. + +This adds one member per entry-point shape. They are the two shapes an EUI +application can have, and they are mutually exclusive per project (features resolve +per consuming project). + +## Members + +| member | `app-main` | entry point | new coverage | +|---|---|---|---| +| `eui-neo-window` | off | hand-written `main()` in the test TU | `core::window`, OpenGL backend, `ScopedRenderBackend`, paced loop, freetype text | +| `eui-neo-app-main` | **on** | upstream `glfw_app_main.cpp` | the feature's source compiles; `main` really comes from the package | + +Both are `[target.'cfg(linux)']`-gated, matching `imgui-window`, `gui-stack` and the +rest of the X11/GLFW stack in this index. Off-Linux they declare no dependencies and +the test TU compiles to `int main() { return 0; }`. + +Both declare `compat.glfw` **directly** rather than leaning on the transitive edge: +each calls GLFW itself (`glfwPollEvents`/`glfwWindowShouldClose` in one, +`glfwSetWindowShouldClose` in the other), because EUI's `core::window` facade +exposes no event-pump entry point. + +## Why the run is opt-in + +A window needs a display; CI runners have none. Same answer as `imgui-window`: +build + link is the headless test, and `MCPP_RUN_WINDOW=1` opts into the real run. + +``` +$ mcpp test -p tests/examples/eui-neo-window # headless CI +compat.eui-neo[own main]: linked; window run is opt-in (MCPP_RUN_WINDOW=1) + +$ MCPP_RUN_WINDOW=1 mcpp test -p tests/examples/eui-neo-window +compat.eui-neo[own main]: ok (60 frames in 0.98s, 120 rect / 120 text draws) +``` + +`eui-neo-app-main` cannot put that gate in `main()` — it does not have one. It goes in +a **namespace-scope constructor**, which runs before the package's `main()`: + +```cpp +struct WindowRunGate { WindowRunGate() { if (!getenv("MCPP_RUN_WINDOW")) exit(0); } }; +const WindowRunGate g_gate; +``` + +Both runs are bounded by a 60-frame budget so the opt-in path terminates on its own +instead of waiting for a human to close the window. + +## Why `app-main` cannot carry tests — and what that pins + +mcpp links a dependency's objects **eagerly**, not as lazily-selected archive members. +`glfw_app_main.o` is therefore always in the link, not only when `main` is still +undefined. Verified by adding a `main()` to an `app-main` project: + +``` +ld: obj/glfw_app_main.o: in function `main': + core/app/glfw_app_main.cpp:398: multiple definition of `main'; + obj/probe.o: first defined here +``` + +So an `app-main` project may not contain **any** TU of its own that defines `main` — +including every `mcpp test` TU. `eui-neo-app-main` is built around that: no `main()`, +per `tests/examples/catch2-main`. This makes the member a two-sided assertion: + +* if `app-main` stops contributing its source → `undefined reference to 'main'` here; +* if `app-main` stops being opt-in → `multiple definition of 'main'` in *every other* + member. + +The descriptor comment on the feature is updated to say this; the previous wording +("a consumer that has its own main() must not get a second one") describes the intent +but understates the consequence. + +## A green test is not a rendered frame + +Per the `eui-neo` plan's "the library really is built" section: a frame counter only +proves `app::update()` ran. `app::update()` reaches `compose()` and the `.onFrame()` +hook without touching the render backend at all, so a member counting frames would +pass against a null backend — which is exactly the failure mode the first revision of +this descriptor shipped (`createRenderBackend()` returning null behind a green CI). + +Both members therefore assert on `core::render::lastRenderFrameStats()` and require +`rectDraws > 0` **and** `textDraws > 0` over the frame budget. `textDraws` is split out +from `rectDraws` on purpose: text is the only path that needs freetype plus a usable +system font, and "no font at any of upstream's Linux fallback paths" is a different +failure from "dead backend". + +## Loop mechanics worth recording + +Two non-obvious things the loop in `eui-neo-window` has to get right, both discovered +by running it: + +* **Pacing is mandatory.** `OpenGLRenderBackend::initialize()` calls + `glfwSwapInterval(0)`, so `present()` never blocks on vsync. An unpaced loop ran at + ~8300 fps and pinned a core; the member paces off `DslAppConfig::fpsValue`. +* **`onFrame` fires twice per rendered frame.** `app::update()` runs the runtime a + second time with `deltaSeconds == 0` to settle the re-compose that `onFrame` itself + requested (`dsl_app_impl.h`, the `composeRequested()` branch). Counting every + invocation reports 2x the real frame rate — gate on the delta. + +Neither is a descriptor bug; both are upstream behaviour a consumer must know, and +neither was discoverable from the headless members. + +## Verification + +Local, mcpp **0.0.109** (matching `validate.yml` `env.MCPP_VERSION`), linux-x86_64, +gcc 16.1.0, X11 display with NVIDIA GL 4.6. + +``` + headless MCPP_RUN_WINDOW=1 +eui-neo-window ok (0.08s) ok (60 frames, 120 rect / 120 text draws) +eui-neo-app-main ok (0.07s) ok (60 frames, 118 rect / 118 text draws) +eui-neo (existing) ok +eui-neo-markdown (existing) ok +``` + +`mcpp xpkg parse`, `check_mirror_urls.lua`, `check_package_name.lua` and +`check_cross_package_refs.lua` all pass on the touched descriptor. The descriptor +change is comment-only. + +## Not done + +* `app-main-sdl2` stays uncovered. It is the same shape against `compat.sdl2`, and + covering it means a fifth window member for one `sources` line; worth doing if the + SDL2 window backend gains real users. +* macOS/Windows only get the build+link half, same as `imgui-window`. The opt-in run + has been exercised on linux-x86_64 only. diff --git a/mcpp.toml b/mcpp.toml index dd1897c..ded7d91 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -20,9 +20,11 @@ members = [ "tests/examples/curl", "tests/examples/eigen", "tests/examples/eui-neo", + "tests/examples/eui-neo-app-main", "tests/examples/eui-neo-markdown", "tests/examples/eui-neo-sdl2", "tests/examples/eui-neo-vulkan", + "tests/examples/eui-neo-window", "tests/examples/ffmpeg", "tests/examples/ffmpeg-module", "tests/examples/fmtlib.fmt", diff --git a/pkgs/e/compat.eui-neo.lua b/pkgs/e/compat.eui-neo.lua index bf31c2f..79df1aa 100644 --- a/pkgs/e/compat.eui-neo.lua +++ b/pkgs/e/compat.eui-neo.lua @@ -259,6 +259,21 @@ package = { -- `compat.gtest`'s `main` feature is: a consumer that has its own -- main() must not get a second one. A real EUI application enables -- this and supplies only app::dslAppConfig() + app::compose(). + -- + -- Sharper than "must not get a second one": mcpp links a + -- dependency's objects EAGERLY, not as lazily-selected archive + -- members, so `glfw_app_main.o` is always in the link rather than + -- only when `main` is still undefined. Enabling this feature is + -- therefore incompatible with ANY translation unit of the consumer + -- that defines main() — including every `mcpp test` TU, which means + -- an app-main project cannot carry its own tests/. Verified: adding + -- one yields `multiple definition of 'main'` from + -- glfw_app_main.cpp:398. tests/examples/eui-neo-app-main is + -- structured around that constraint (no main() at all, and its + -- opt-in window run is gated in a namespace-scope constructor + -- because there is no main() of ours to gate it in); + -- tests/examples/eui-neo-window is the same UI with the feature OFF + -- and a hand-written loop. ["app-main"] = { sources = { "*/core/app/glfw_app_main.cpp" } }, -- Same gate for the SDL2 window backend. Upstream picks between the -- two by EUI_APP_MAIN_SOURCE; here the consumer picks by name, and diff --git a/tests/examples/eui-neo-app-main/mcpp.toml b/tests/examples/eui-neo-app-main/mcpp.toml new file mode 100644 index 0000000..7a1a2fb --- /dev/null +++ b/tests/examples/eui-neo-app-main/mcpp.toml @@ -0,0 +1,27 @@ +# compat.eui-neo, `app-main` FEATURE path: the package supplies `int main()` +# (upstream core/app/glfw_app_main.cpp) and this project deliberately defines +# none. If the feature stops contributing that source, this member fails to link +# with `undefined reference to 'main'` — the same assertion shape as +# tests/examples/catch2-main. +# +# This cannot share a project with tests/examples/eui-neo: features are resolved +# per consuming project, and — because mcpp links a dependency's objects +# EAGERLY rather than as lazily-selected archive members — a project that +# enables `app-main` may not contain any translation unit of its own that +# defines main(). See the note in pkgs/e/compat.eui-neo.lua. +# +# Linux-only (cfg): the whole X11/GLFW/GL stack is linux-gated across this +# index, matching tests/examples/imgui-window. Off-Linux the member has no +# dependencies and the test TU compiles to a no-op. +# +# compat.glfw is a DIRECT dependency because the test TU calls +# glfwSetWindowShouldClose() itself to bound the opt-in window run. +# +# The `compat` index redirect is inherited from the workspace root. +[package] +name = "eui-neo-app-main-tests" +version = "0.1.0" + +[target.'cfg(linux)'.dependencies.compat] +eui-neo = { version = "0.5.3", features = ["app-main"] } +glfw = "3.4" diff --git a/tests/examples/eui-neo-app-main/tests/app_main.cpp b/tests/examples/eui-neo-app-main/tests/app_main.cpp new file mode 100644 index 0000000..5f2a38f --- /dev/null +++ b/tests/examples/eui-neo-app-main/tests/app_main.cpp @@ -0,0 +1,172 @@ +// compat.eui-neo `app-main` FEATURE: there is no main() in this file on purpose. +// The entry point is upstream's core/app/glfw_app_main.cpp, which the feature +// adds to the library — it owns the window, the GL backend and the render loop, +// and calls back into the two symbols an EUI application must supply: +// app::dslAppConfig() and app::compose(). That is the whole shape being pinned +// here, and it is the shape a real EUI-NEO application uses. +// +// tests/examples/eui-neo-window asserts the OTHER half of the same feature pair: +// the same UI driven by a hand-written main(), with `app-main` NOT enabled. +// +// Two things make this member work under `mcpp test`: +// +// 1. The link IS the primary assertion. `main` has to come from the package; +// if the feature ever stops gating glfw_app_main.cpp in, this member fails +// with `undefined reference to 'main'`. Conversely, if the feature ever +// stopped being opt-in, every OTHER member would fail with +// `multiple definition of 'main'` — mcpp links a dependency's objects +// eagerly, so there is no lazy-archive escape hatch. +// +// 2. Running a window needs a display, so the run is opt-in via +// MCPP_RUN_WINDOW=1, as in tests/examples/imgui-window. Because the entry +// point belongs to the package, the gate cannot live in main() — it lives in +// a namespace-scope constructor, which runs before it. Headless CI links the +// real binary, executes the guard, and exits 0. +#if defined(__linux__) + +#include + +#include "core/render/render_backend.h" + +#include +import std; + +namespace { + +// Frames to render before asking the framework's loop to exit, so the opt-in run +// terminates on its own instead of waiting for a human to close the window. +constexpr int kFrameBudget = 60; + +int g_frames = 0; + +// Accumulated from the PREVIOUS frame's render stats — the framework's loop +// publishes them after each present(), and compose() is the only place an +// app-main consumer gets to look. A frame counter alone would pass against a +// null backend; these prove the OpenGL backend issued real geometry. +long long g_rectDraws = 0; +long long g_textDraws = 0; + +// Runs before the package's main(). See point 2 above. +struct WindowRunGate { + WindowRunGate() { + if (std::getenv("MCPP_RUN_WINDOW") != nullptr) { + return; + } + std::println("compat.eui-neo[app-main]: linked, main() supplied by the package; " + "window run is opt-in (MCPP_RUN_WINDOW=1)"); + std::exit(0); + } +}; + +const WindowRunGate g_gate; + +} // namespace + +namespace app { + +const DslAppConfig& dslAppConfig() { + static const DslAppConfig config = DslAppConfig{} + .title("compat.eui-neo — app-main") + .pageId("eui_neo_app_main") + .clearColor({0.06f, 0.07f, 0.09f, 1.0f}) + .windowSize(420, 240) + .fps(60.0) + .showDebugStatsInTitle(false); + return config; +} + +void compose(eui::Ui& ui, const eui::Screen& screen) { + // A node carrying .onFrame() marks the runtime as animating, which is what + // keeps the framework's loop rendering continuously instead of parking in + // glfwWaitEvents(). It is also the only per-frame hook an app-main consumer + // has, so the frame budget is enforced from here. + ui.stack("loop.driver") + .size(1.0f, 1.0f) + .onFrame([](float deltaSeconds) { + // app::update() runs the runtime twice per frame — the second pass + // with deltaSeconds == 0, to settle the re-compose that onFrame + // itself requested — so this fires twice per rendered frame. + if (deltaSeconds <= 0.0f) { + return; + } + + const core::render::RenderFrameStats& stats = core::render::lastRenderFrameStats(); + g_rectDraws += stats.rectDraws; + g_textDraws += stats.textDraws; + + if (++g_frames < kFrameBudget) { + return; + } + + // The entry point belongs to the package, so there is no main() of + // ours to return a code from — exit() is how this member fails. + if (g_rectDraws <= 0) { + std::println("no rect geometry reached the backend over {} frames", g_frames); + std::exit(2); + } + if (g_textDraws <= 0) { + // Text needs the freetype path AND a usable font; worth + // distinguishing from a dead backend. + std::println("no text geometry reached the backend over {} frames " + "(freetype path or system font missing)", g_frames); + std::exit(3); + } + + std::println("compat.eui-neo[app-main]: ok ({} frames, {} rect / {} text draws)", + g_frames, g_rectDraws, g_textDraws); + + // The framework's loop keeps our window's context current across + // update and render, so this is the window it is driving. + if (GLFWwindow* window = glfwGetCurrentContext()) { + glfwSetWindowShouldClose(window, GLFW_TRUE); + } + }) + .build(); + + // Text exercises the freetype + font-fallback path, rects the OpenGL + // primitive path — i.e. the parts a headless smoke test cannot reach. + ui.text("title") + .position(32.0f, 40.0f) + .size(std::max(120.0f, screen.width - 64.0f), 30.0f) + .text("app-main") + .fontSize(22.0f) + .lineHeight(28.0f) + .color({0.94f, 0.96f, 1.0f, 1.0f}) + .build(); + + ui.text("stats") + .position(32.0f, 76.0f) + .size(std::max(120.0f, screen.width - 64.0f), 24.0f) + .text(std::format("frame {} / {}", g_frames, kFrameBudget)) + .fontSize(15.0f) + .lineHeight(20.0f) + .color({0.58f, 0.68f, 0.80f, 1.0f}) + .build(); + + const float progress = static_cast(g_frames) / static_cast(kFrameBudget); + const float trackWidth = std::max(120.0f, screen.width - 64.0f); + + ui.rect("track") + .position(32.0f, 150.0f) + .size(trackWidth, 6.0f) + .radius(3.0f) + .color({0.16f, 0.19f, 0.24f, 1.0f}) + .build(); + + ui.rect("fill") + .position(32.0f, 150.0f) + .size(std::min(1.0f, progress) * trackWidth, 6.0f) + .radius(3.0f) + .color({0.36f, 0.62f, 0.98f, 1.0f}) + .build(); +} + +} // namespace app + +#else + +// Off-Linux the member declares no dependencies, so there is nothing to link +// against and no package-supplied main() either — this TU has to provide one. +int main() { return 0; } + +#endif diff --git a/tests/examples/eui-neo-window/mcpp.toml b/tests/examples/eui-neo-window/mcpp.toml new file mode 100644 index 0000000..2f262f7 --- /dev/null +++ b/tests/examples/eui-neo-window/mcpp.toml @@ -0,0 +1,26 @@ +# compat.eui-neo driven by a HAND-WRITTEN main() — `app-main` deliberately NOT +# enabled, so the package contributes no entry point and this project owns the +# window, the render backend and the loop. +# +# The pair matters: tests/examples/eui-neo-app-main asserts the framework-main +# shape, this member asserts the own-main shape. Nothing else in this index +# builds an eui-neo WINDOW at all — tests/examples/eui-neo is headless by +# construction (json + platform flags), so window creation, the OpenGL backend, +# the text/freetype path and the render loop were previously uncovered. +# +# Linux-only (cfg), matching tests/examples/imgui-window and the rest of the +# X11/GLFW stack in this index. The window run needs a display and is opt-in +# (MCPP_RUN_WINDOW=1); headless CI compiles + links the whole loop and returns 0. +# +# compat.glfw is a DIRECT dependency, not just a transitive one: this project's +# own translation unit calls glfwPollEvents()/glfwWindowShouldClose(), because +# EUI's core::window facade exposes no event-pump entry point. +# +# The `compat` index redirect is inherited from the workspace root. +[package] +name = "eui-neo-window-tests" +version = "0.1.0" + +[target.'cfg(linux)'.dependencies.compat] +eui-neo = "0.5.3" +glfw = "3.4" diff --git a/tests/examples/eui-neo-window/tests/window.cpp b/tests/examples/eui-neo-window/tests/window.cpp new file mode 100644 index 0000000..862d9a6 --- /dev/null +++ b/tests/examples/eui-neo-window/tests/window.cpp @@ -0,0 +1,312 @@ +// compat.eui-neo with a HAND-WRITTEN main(): the own-main shape, and the only +// place in this index that builds an eui-neo window. +// +// tests/examples/eui-neo covers the headless surface (json, platform flags) and +// tests/examples/eui-neo-app-main covers the framework-main shape. What is left, +// and what this member pins, is everything between window creation and a +// presented frame: core::window::createWindow(), the OpenGL render backend, +// app::initialize/update/render, ScopedRenderBackend, and the freetype text path. +// +// Without `app-main` the package contributes no entry point, so the five things +// upstream's glfw_app_main.cpp does have to be done by hand — see runWindow(). +// That is the point: a consumer must be able to drive EUI itself, and the public +// surface it needs (core::window, core::render, app::*) must actually be reachable +// through the descriptor's include_dirs. +// +// The window run needs a display, so it is opt-in via MCPP_RUN_WINDOW=1, exactly +// as in tests/examples/imgui-window. Headless CI compiles and links the whole +// loop — which is where a descriptor regression would surface — and returns 0. +#if defined(__linux__) + +#include + +#include "core/input/input_state.h" +#include "core/platform/platform.h" +#include "core/render/render_backend.h" +#include "core/window/window_backend.h" + +#include + +import std; + +namespace { + +// Rendered frames to require before exiting, so the opt-in run terminates on its +// own. Also the assertion: fewer than this means the loop stalled. +constexpr int kFrameBudget = 60; + +struct Loop { + float elapsed = 0.0f; + int frames = 0; + // Accumulated from core::render::lastRenderFrameStats(). A frame counter only + // proves app::update() ran — these prove the OpenGL backend actually issued + // geometry, i.e. that the render half is wired too. Without them this member + // would pass just as happily against a null backend. + long long rectDraws = 0; + long long textDraws = 0; +}; + +Loop g_loop; + +} // namespace + +namespace app { + +const DslAppConfig& dslAppConfig() { + static const DslAppConfig config = DslAppConfig{} + .title("compat.eui-neo — own main") + .pageId("eui_neo_own_main") + .clearColor({0.06f, 0.07f, 0.09f, 1.0f}) + .windowSize(420, 240) + .fps(60.0) + .showDebugStatsInTitle(false); + return config; +} + +void compose(eui::Ui& ui, const eui::Screen& screen) { + // A node carrying .onFrame() marks the runtime as animating, which is what + // turns a redraw-on-demand retained-mode UI into a continuously running loop. + ui.stack("loop.driver") + .size(1.0f, 1.0f) + .onFrame([](float deltaSeconds) { + // app::update() runs the runtime twice per frame — the second pass + // with deltaSeconds == 0, to settle the re-compose that onFrame + // itself requested — so this fires twice per rendered frame. + if (deltaSeconds <= 0.0f) { + return; + } + g_loop.elapsed += deltaSeconds; + ++g_loop.frames; + }) + .build(); + + // Text exercises the freetype + font-fallback path, rects the OpenGL + // primitive path. + const float trackWidth = std::max(120.0f, screen.width - 64.0f); + + ui.text("title") + .position(32.0f, 40.0f) + .size(trackWidth, 30.0f) + .text("own main") + .fontSize(22.0f) + .lineHeight(28.0f) + .color({0.94f, 0.96f, 1.0f, 1.0f}) + .build(); + + ui.text("stats") + .position(32.0f, 76.0f) + .size(trackWidth, 24.0f) + .text(std::format("frame {} / {} t = {:.2f}s", g_loop.frames, kFrameBudget, g_loop.elapsed)) + .fontSize(15.0f) + .lineHeight(20.0f) + .color({0.58f, 0.68f, 0.80f, 1.0f}) + .build(); + + const float progress = static_cast(g_loop.frames) / static_cast(kFrameBudget); + + ui.rect("track") + .position(32.0f, 150.0f) + .size(trackWidth, 6.0f) + .radius(3.0f) + .color({0.16f, 0.19f, 0.24f, 1.0f}) + .build(); + + ui.rect("fill") + .position(32.0f, 150.0f) + .size(std::min(1.0f, progress) * trackWidth, 6.0f) + .radius(3.0f) + .color({0.98f, 0.55f, 0.36f, 1.0f}) + .build(); +} + +} // namespace app + +namespace { + +float dpiScaleOf(GLFWwindow* window) { + float scaleX = 1.0f; + float scaleY = 1.0f; + glfwGetWindowContentScale(window, &scaleX, &scaleY); + return (scaleX + scaleY) * 0.5f; +} + +// Framebuffer pixels per window point; EUI needs it to map cursor coordinates +// onto the framebuffer. +float pointerScaleOf(GLFWwindow* window) { + int windowWidth = 0; + int windowHeight = 0; + int framebufferWidth = 0; + int framebufferHeight = 0; + glfwGetWindowSize(window, &windowWidth, &windowHeight); + glfwGetFramebufferSize(window, &framebufferWidth, &framebufferHeight); + if (windowWidth <= 0 || windowHeight <= 0) { + return 1.0f; + } + return 0.5f * (static_cast(framebufferWidth) / static_cast(windowWidth) + + static_cast(framebufferHeight) / static_cast(windowHeight)); +} + +int runWindow() { + core::platform::repairCurrentWorkingDirectory(); + core::render::initializeRenderBackendLoader(); + + glfwSetErrorCallback([](int code, const char* description) { + std::println("glfw error {}: {}", code, description != nullptr ? description : ""); + }); + + if (!glfwInit()) { + std::println("glfwInit() failed"); + return 10; + } + + const app::DslAppConfig& config = app::dslAppConfig(); + + core::window::WindowCreateRequest request; + request.width = config.windowWidthValue; + request.height = config.windowHeightValue; + request.title = config.titleValue.c_str(); + request.renderApi = core::render::windowRenderApi(); + + auto* window = static_cast(core::window::createWindow(request)); + if (window == nullptr) { + std::println("core::window::createWindow() failed"); + glfwTerminate(); + return 11; + } + + auto backend = core::render::createRenderBackend(window); + if (!backend || !backend->initialize()) { + std::println("core::render::createRenderBackend() failed"); + core::window::destroyWindow(window); + glfwTerminate(); + return 12; + } + + if (!app::initialize(window)) { + std::println("app::initialize() failed"); + backend.reset(); + core::window::destroyWindow(window); + glfwTerminate(); + return 13; + } + + // Frame pacing is NOT optional in a hand-written loop: the OpenGL backend + // calls glfwSwapInterval(0) in initialize(), so present() never blocks on + // vsync and an unpaced loop runs at thousands of fps and pins a core. + // Upstream's main does the same thing with monitor refresh-rate detection on + // top. + const double frameInterval = config.fpsValue > 0.0 ? 1.0 / config.fpsValue : 0.0; + double lastFrameTime = core::window::timeSeconds(); + double nextFrameTime = lastFrameTime; + + while (!glfwWindowShouldClose(window) && g_loop.frames < kFrameBudget) { + glfwPollEvents(); + + const double now = core::window::timeSeconds(); + const float deltaSeconds = static_cast(now - lastFrameTime); + lastFrameTime = now; + + int framebufferWidth = 0; + int framebufferHeight = 0; + glfwGetFramebufferSize(window, &framebufferWidth, &framebufferHeight); + if (framebufferWidth <= 0 || framebufferHeight <= 0) { + glfwWaitEvents(); + lastFrameTime = core::window::timeSeconds(); + nextFrameTime = lastFrameTime; + continue; + } + + const float dpiScale = dpiScaleOf(window); + const float pointerScale = pointerScaleOf(window); + + backend->makeCurrent(); + + // Layout + compose() + input dispatch; also fires the .onFrame() + // callback that advances g_loop. + app::update(window, deltaSeconds, framebufferWidth, framebufferHeight, dpiScale, pointerScale); + + backend->beginFrame({ + window, + core::window::nativeWindowInfo(window), + framebufferWidth, + framebufferHeight, + dpiScale + }); + { + // app::render() draws through core::render::activeRenderBackend(); + // this guard is what installs ours as the active one. Without it the + // frame is silently dropped. + core::render::ScopedRenderBackend scopedBackend(*backend); + app::render(framebufferWidth, framebufferHeight, dpiScale); + } + backend->present(); + + // app::render() -> Runtime::render() calls beginRenderFrameStats(); this + // moves the counters into lastRenderFrameStats() so they can be read. + core::render::publishRenderFrameStats(); + const core::render::RenderFrameStats& stats = core::render::lastRenderFrameStats(); + g_loop.rectDraws += stats.rectDraws; + g_loop.textDraws += stats.textDraws; + + nextFrameTime += frameInterval; + const double slack = nextFrameTime - core::window::timeSeconds(); + if (slack > 0.0) { + std::this_thread::sleep_for(std::chrono::duration(slack)); + } else { + // Fell behind: resynchronise instead of accumulating debt. + nextFrameTime = core::window::timeSeconds(); + } + } + + core::releaseInputQueue(window); + backend->makeCurrent(); + backend->releaseRenderCache(); + { + core::render::ScopedRenderBackend scopedBackend(*backend); + app::shutdown(); + } + backend.reset(); + core::window::destroyWindow(window); + glfwTerminate(); + + if (g_loop.frames < kFrameBudget) { + std::println("loop stalled: {} of {} frames in {:.2f}s", + g_loop.frames, kFrameBudget, g_loop.elapsed); + return 14; + } + if (g_loop.rectDraws <= 0) { + std::println("no rect geometry reached the backend over {} frames", g_loop.frames); + return 15; + } + if (g_loop.textDraws <= 0) { + // Text needs the freetype path AND a usable font. On a box with no + // system font at any of upstream's Linux fallback paths this is the + // failure you get, and it is worth distinguishing from a dead backend. + std::println("no text geometry reached the backend over {} frames " + "(freetype path or system font missing)", g_loop.frames); + return 16; + } + + std::println("compat.eui-neo[own main]: ok ({} frames in {:.2f}s, {} rect / {} text draws)", + g_loop.frames, g_loop.elapsed, g_loop.rectDraws, g_loop.textDraws); + return 0; +} + +} // namespace + +int main() { + // Compiling + linking runWindow() is the headless test: it is the only thing + // in this index that instantiates core::window / core::render / app::* from a + // consumer TU. The run itself needs a display (opt-in). + if (std::getenv("MCPP_RUN_WINDOW") != nullptr) { + return runWindow(); + } + std::println("compat.eui-neo[own main]: linked; window run is opt-in (MCPP_RUN_WINDOW=1)"); + return 0; +} + +#else + +int main() { return 0; } + +#endif From 675a544905a403a33618eeb7fd9c2ddd92efe865 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Thu, 30 Jul 2026 08:19:42 +0800 Subject: [PATCH 2/4] test(eui-neo): un-gate both window members to all three platforms MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both members were [target.'cfg(linux)']-gated, which made them compile to `int main() { return 0; }` on macOS and Windows — trivially green, zero coverage, and it left `glfw_app_main.cpp` uncompiled on every platform after all. imgui-window and gui-stack are linux-only because they pull the X11 packages directly. compat.eui-neo and compat.glfw are not, and the four existing eui-neo members already build on every runner. Audited before un-gating: every core/ header these TUs reach (window_types, window_backend, render_backend, primitive_geometry, render_surface, platform, input_state, input_types, ime_bridge) is platform-clean — no windows.h, no Cocoa, no GL — and every GLFW entry point used is portable. core::releaseInputQueue reaches eui_ime_uninstall_message_filter, which ime_bridge.c defines on all three. Also add GLFW_INCLUDE_NONE before , as core/input/input_state.h does, so GLFW does not pull a GL header of its own choosing (GL/gl.h on Windows, the deprecated OpenGL/gl.h on macOS). Drop the redundant `glfw = "3.4"` from both members. mcpp propagates a transitive dependency's include_dirs and link inputs to the consumer, and compat.eui-neo already depends on compat.glfw everywhere — verified by removing the line and rebuilding. Matches the existing convention: eui-neo-sdl2 includes / and eui-neo-vulkan calls vkEnumerateInstanceVersion, neither re-declaring those packages. macOS/Windows are CI-verified only; there is no runner for either here. The opt-in windowed run stays linux-only (both runners are headless), same as imgui-window. Descriptor comment fix while re-probing the encoding: it claimed "there is no `default-features = false` (mcpp#242)". Wrong — #242 shipped in 0.0.98. The accurate statement is that its seedDefault gate is manifest-side and a `default` feature declared in an xpkg DESCRIPTOR is never seeded at all, so the consumer has nothing to switch off. Re-probed with `default = { defines = { ... } }` and a plain consumer: macro absent on 0.0.109. MCPP_VERSION deliberately stays at 0.0.109: #242 (0.0.98), #243 (0.0.99) and #233/#240 (0.0.97/0.0.98) are all at or below the pin, and nothing in 2026.7.27.1 .. 2026.7.29.2 touches feature semantics, so a bump buys no simplification here. --- ...-30-eui-neo-window-and-app-main-members.md | 79 ++++++++++++++++--- pkgs/e/compat.eui-neo.lua | 16 +++- tests/examples/eui-neo-app-main/mcpp.toml | 20 +++-- .../eui-neo-app-main/tests/app_main.cpp | 21 ++--- tests/examples/eui-neo-window/mcpp.toml | 27 +++++-- .../examples/eui-neo-window/tests/window.cpp | 19 +++-- 6 files changed, 133 insertions(+), 49 deletions(-) diff --git a/.agents/docs/2026-07-30-eui-neo-window-and-app-main-members.md b/.agents/docs/2026-07-30-eui-neo-window-and-app-main-members.md index a73ebf8..0c5f7af 100644 --- a/.agents/docs/2026-07-30-eui-neo-window-and-app-main-members.md +++ b/.agents/docs/2026-07-30-eui-neo-window-and-app-main-members.md @@ -39,14 +39,28 @@ per consuming project). | `eui-neo-window` | off | hand-written `main()` in the test TU | `core::window`, OpenGL backend, `ScopedRenderBackend`, paced loop, freetype text | | `eui-neo-app-main` | **on** | upstream `glfw_app_main.cpp` | the feature's source compiles; `main` really comes from the package | -Both are `[target.'cfg(linux)']`-gated, matching `imgui-window`, `gui-stack` and the -rest of the X11/GLFW stack in this index. Off-Linux they declare no dependencies and -the test TU compiles to `int main() { return 0; }`. - -Both declare `compat.glfw` **directly** rather than leaning on the transitive edge: -each calls GLFW itself (`glfwPollEvents`/`glfwWindowShouldClose` in one, -`glfwSetWindowShouldClose` in the other), because EUI's `core::window` facade -exposes no event-pump entry point. +Both build on **all three platforms**, deliberately not `cfg`-gated. `imgui-window` +and `gui-stack` are linux-only because they pull the X11 packages directly; +`compat.eui-neo` and `compat.glfw` are not, and the four existing eui-neo members +already build on every runner. Audited before un-gating: every `core/` header these +TUs reach (`window_types.h`, `window_backend.h`, `render_backend.h`, +`primitive_geometry.h`, `render_surface.h`, `platform.h`, `input_state.h`, +`input_types.h`, `ime_bridge.h`) is platform-clean — no `windows.h`, no Cocoa, no GL +— and every GLFW entry point used is portable. `core::releaseInputQueue` reaches +`eui_ime_uninstall_message_filter`, which `ime_bridge.c` defines on all three +platforms. + +Both add `GLFW_INCLUDE_NONE` before ``, as `core/input/input_state.h` +does, so GLFW does not pull a GL header of its own choosing (`GL/gl.h` on Windows, +the deprecated `OpenGL/gl.h` on macOS). EUI reaches GL through `compat.glad`. + +Neither re-declares `compat.glfw`, even though both call GLFW directly (EUI's +`core::window` facade exposes no event-pump entry point). mcpp propagates a +transitive dependency's `include_dirs` and link inputs to the consumer, and +`compat.eui-neo` already depends on `compat.glfw` on every platform. Verified by +dropping the line and rebuilding both members. This matches the existing +convention: `eui-neo-sdl2` includes `` and ``, `eui-neo-vulkan` +calls `vkEnumerateInstanceVersion`, and neither declares those packages. ## Why the run is opt-in @@ -126,6 +140,32 @@ by running it: Neither is a descriptor bug; both are upstream behaviour a consumer must know, and neither was discoverable from the headless members. +## Why the mcpp pin does NOT move + +Checked before writing any of this, because the encoding in this descriptor is ugly +enough to be worth re-testing against a newer client. `MCPP_VERSION` stays at +**0.0.109**. + +The three capabilities that could simplify `compat.eui-neo` all landed **at or before +0.0.109**, which is already the pin: `default-features = false` (#242, 0.0.98), +feature→feature forwarding (#243, 0.0.99), obj-path disambiguation (#233/#240, +0.0.97/0.0.98). The four releases after it — `2026.7.27.1`, `.28.2`, `.29.1`, +`.29.2` — carry the date-based version scheme, the private-glibc `LD_LIBRARY_PATH` +fix (#291), 4-segment version parsing, xlings pin hygiene, `[build].defines` before +the P1689 scan (#297), the musl/MinGW `build.mcpp` host helper (#298), `mcpp add` +index validation (#307) and SemVer routing (#309). `git log v0.0.109..HEAD` over the +feature/resolver/plan sources returns nothing that touches feature semantics. + +One correction to the descriptor's own comment while re-probing: it claimed "there is +no `default-features = false` (mcpp#242)". That is wrong — #242 shipped in 0.0.98. +The accurate statement is that its `seedDefault` gate is **manifest**-side, and a +`default` feature declared in an xpkg **descriptor** is never seeded at all, so the +consumer has nothing to switch off. Re-probed by giving the package +`default = { defines = { "MCPP_PROBE_DEFAULT_APPLIED=1" } }` and checking the macro +from a plain consumer TU: absent on 0.0.109. The generated-header + forced-`-include` +encoding therefore stays as it is. Worth filing upstream as a descriptor-side gap in +#242's coverage. + ## Verification Local, mcpp **0.0.109** (matching `validate.yml` `env.MCPP_VERSION`), linux-x86_64, @@ -133,8 +173,8 @@ gcc 16.1.0, X11 display with NVIDIA GL 4.6. ``` headless MCPP_RUN_WINDOW=1 -eui-neo-window ok (0.08s) ok (60 frames, 120 rect / 120 text draws) -eui-neo-app-main ok (0.07s) ok (60 frames, 118 rect / 118 text draws) +eui-neo-window ok (0.08s) ok (60 frames in 0.98s, 120 rect / 120 text draws) +eui-neo-app-main ok (0.08s) ok (60 frames, 118 rect / 118 text draws) eui-neo (existing) ok eui-neo-markdown (existing) ok ``` @@ -143,10 +183,25 @@ eui-neo-markdown (existing) ok `check_cross_package_refs.lua` all pass on the touched descriptor. The descriptor change is comment-only. +**macOS and Windows are CI-verified only.** There is no runner for either here, so +the build+link half of both members on those platforms is asserted by the +`workspace (macos)` / `workspace (windows)` legs, not locally. Two things are new +there and worth watching on the first run: + +* `glfw_app_main.cpp` has never been compiled on any platform. Its Windows half + (``, `GLFW_EXPOSE_NATIVE_WIN32`, `glfw3native.h`, `timeBeginPeriod`, + `MonitorFromWindow`, `EnumDisplaySettingsW`) needs `-lwinmm` and `-luser32`, both + already in the descriptor's `windows.ldflags`. +* these are the first members to link `compat.glfw` off Linux from their **own** code + rather than through `compat.eui-neo`, which exercises the macOS + `Cocoa`/`IOKit`/`CoreFoundation` frameworks and the Windows `-lgdi32` from a + consumer link. + ## Not done * `app-main-sdl2` stays uncovered. It is the same shape against `compat.sdl2`, and covering it means a fifth window member for one `sources` line; worth doing if the SDL2 window backend gains real users. -* macOS/Windows only get the build+link half, same as `imgui-window`. The opt-in run - has been exercised on linux-x86_64 only. +* The **opt-in windowed run** has been exercised on linux-x86_64 only. macOS and + Windows runners are headless, so they get build+link only — same as + `imgui-window`. diff --git a/pkgs/e/compat.eui-neo.lua b/pkgs/e/compat.eui-neo.lua index 79df1aa..2537040 100644 --- a/pkgs/e/compat.eui-neo.lua +++ b/pkgs/e/compat.eui-neo.lua @@ -174,10 +174,18 @@ package = { -- / else-GLFW. Define both halves of either pair and the first one -- silently wins, ignoring what the consumer asked for. -- - -- mcpp features are purely additive and there is no - -- `default-features = false` (mcpp#242). The obvious encodings all - -- fail on 0.0.109, each in its own way — all three verified with - -- probes, because each failure is silent: + -- mcpp features are purely additive here. `default-features = false` + -- does exist (mcpp#242, since 0.0.98) — but its `seedDefault` gate lives + -- on the MANIFEST side, and a `default` feature declared in an xpkg + -- DESCRIPTOR is never seeded to begin with, so there is nothing for the + -- consumer to switch off. Re-probed on 0.0.109 by giving this package a + -- `default = { defines = {...} }` and checking the macro from a plain + -- consumer: absent. Unchanged in the newest mcpp (2026.7.29.2) — nothing + -- has touched the feature system since 0.0.109, so a version bump buys + -- no simplification of the encoding below. + -- + -- The obvious encodings all fail on 0.0.109, each in its own way — all + -- three verified with probes, because each failure is silent: -- -- * `default = { defines/sources/deps = … }` is INERT. Not -- "suppressed when features are named" — never applied at all. A diff --git a/tests/examples/eui-neo-app-main/mcpp.toml b/tests/examples/eui-neo-app-main/mcpp.toml index 7a1a2fb..6c70ab1 100644 --- a/tests/examples/eui-neo-app-main/mcpp.toml +++ b/tests/examples/eui-neo-app-main/mcpp.toml @@ -10,18 +10,24 @@ # enables `app-main` may not contain any translation unit of its own that # defines main(). See the note in pkgs/e/compat.eui-neo.lua. # -# Linux-only (cfg): the whole X11/GLFW/GL stack is linux-gated across this -# index, matching tests/examples/imgui-window. Off-Linux the member has no -# dependencies and the test TU compiles to a no-op. +# ALL THREE PLATFORMS, deliberately not cfg-gated. `app-main` selects upstream's +# GLFW entry point, which is the default window backend on every platform, and +# nothing in it is linux-specific — but until this member existed it was compiled +# on NONE of them, so the windows `-fno-char8_t` / winmm+user32+pdh half of the +# descriptor and the macOS Cocoa link were both unexercised for that TU. # -# compat.glfw is a DIRECT dependency because the test TU calls -# glfwSetWindowShouldClose() itself to bound the opt-in window run. +# compat.glfw is NOT re-declared here even though this TU calls +# glfwSetWindowShouldClose()/glfwGetCurrentContext() itself to bound the opt-in +# window run: mcpp propagates a transitive dependency's `include_dirs` and link +# inputs to the consumer, and compat.eui-neo already depends on compat.glfw on +# every platform. Same convention as tests/examples/eui-neo-sdl2 and +# eui-neo-vulkan, which include / without re-declaring +# either. # # The `compat` index redirect is inherited from the workspace root. [package] name = "eui-neo-app-main-tests" version = "0.1.0" -[target.'cfg(linux)'.dependencies.compat] +[dependencies.compat] eui-neo = { version = "0.5.3", features = ["app-main"] } -glfw = "3.4" diff --git a/tests/examples/eui-neo-app-main/tests/app_main.cpp b/tests/examples/eui-neo-app-main/tests/app_main.cpp index 5f2a38f..c6530e0 100644 --- a/tests/examples/eui-neo-app-main/tests/app_main.cpp +++ b/tests/examples/eui-neo-app-main/tests/app_main.cpp @@ -22,13 +22,22 @@ // point belongs to the package, the gate cannot live in main() — it lives in // a namespace-scope constructor, which runs before it. Headless CI links the // real binary, executes the guard, and exits 0. -#if defined(__linux__) - +// +// Builds on all three platforms: GLFW is upstream's default window backend +// everywhere, and nothing below is platform-specific. #include #include "core/render/render_backend.h" +// GLFW would otherwise pull a GL header of its own choosing (GL/gl.h on Windows, +// the deprecated OpenGL/gl.h on macOS). EUI reaches GL through compat.glad, so +// this TU wants the GLFW API and nothing else — the same thing +// core/input/input_state.h does before its own include. +#ifndef GLFW_INCLUDE_NONE +#define GLFW_INCLUDE_NONE +#endif #include + import std; namespace { @@ -162,11 +171,3 @@ void compose(eui::Ui& ui, const eui::Screen& screen) { } } // namespace app - -#else - -// Off-Linux the member declares no dependencies, so there is nothing to link -// against and no package-supplied main() either — this TU has to provide one. -int main() { return 0; } - -#endif diff --git a/tests/examples/eui-neo-window/mcpp.toml b/tests/examples/eui-neo-window/mcpp.toml index 2f262f7..8dcbbc8 100644 --- a/tests/examples/eui-neo-window/mcpp.toml +++ b/tests/examples/eui-neo-window/mcpp.toml @@ -8,19 +8,30 @@ # construction (json + platform flags), so window creation, the OpenGL backend, # the text/freetype path and the render loop were previously uncovered. # -# Linux-only (cfg), matching tests/examples/imgui-window and the rest of the -# X11/GLFW stack in this index. The window run needs a display and is opt-in -# (MCPP_RUN_WINDOW=1); headless CI compiles + links the whole loop and returns 0. +# ALL THREE PLATFORMS, deliberately not cfg-gated. Unlike tests/examples/ +# imgui-window and gui-stack — linux-only because they pull the X11 packages +# directly — compat.eui-neo and compat.glfw both build on linux/macosx/windows, +# and the four existing eui-neo members already prove that on every runner. What +# was never compiled anywhere is a CONSUMER translation unit that includes +# core/window, core/render and , which is exactly what this member +# is. None of the headers it reaches carry platform-specific includes. # -# compat.glfw is a DIRECT dependency, not just a transitive one: this project's -# own translation unit calls glfwPollEvents()/glfwWindowShouldClose(), because -# EUI's core::window facade exposes no event-pump entry point. +# The window run needs a display and is opt-in (MCPP_RUN_WINDOW=1); headless CI +# compiles + links the whole loop on each OS and returns 0. +# +# compat.glfw is NOT re-declared here even though this TU calls glfwPollEvents() +# and glfwWindowShouldClose() itself (EUI's core::window facade exposes no +# event-pump entry point). mcpp propagates a transitive dependency's +# `include_dirs` and link inputs to the consumer, so the edge through +# compat.eui-neo is enough — verified by dropping the line and rebuilding. Same +# convention as tests/examples/eui-neo-sdl2 (`#include `, ``) +# and eui-neo-vulkan (`vkEnumerateInstanceVersion`), neither of which re-declares +# what it reaches through a feature. # # The `compat` index redirect is inherited from the workspace root. [package] name = "eui-neo-window-tests" version = "0.1.0" -[target.'cfg(linux)'.dependencies.compat] +[dependencies.compat] eui-neo = "0.5.3" -glfw = "3.4" diff --git a/tests/examples/eui-neo-window/tests/window.cpp b/tests/examples/eui-neo-window/tests/window.cpp index 862d9a6..b11b6ae 100644 --- a/tests/examples/eui-neo-window/tests/window.cpp +++ b/tests/examples/eui-neo-window/tests/window.cpp @@ -16,8 +16,10 @@ // The window run needs a display, so it is opt-in via MCPP_RUN_WINDOW=1, exactly // as in tests/examples/imgui-window. Headless CI compiles and links the whole // loop — which is where a descriptor regression would surface — and returns 0. -#if defined(__linux__) - +// +// Builds on all three platforms. Every core/ header below is platform-clean (no +// windows.h, no Cocoa, no GL), and every GLFW entry point used here is portable, +// so there is no reason to cfg-gate this the way the X11-consuming members are. #include #include "core/input/input_state.h" @@ -25,6 +27,13 @@ #include "core/render/render_backend.h" #include "core/window/window_backend.h" +// GLFW would otherwise pull a GL header of its own choosing (GL/gl.h on Windows, +// the deprecated OpenGL/gl.h on macOS). EUI reaches GL through compat.glad, so +// this TU wants the GLFW API and nothing else — the same thing +// core/input/input_state.h does before its own include. +#ifndef GLFW_INCLUDE_NONE +#define GLFW_INCLUDE_NONE +#endif #include import std; @@ -304,9 +313,3 @@ int main() { std::println("compat.eui-neo[own main]: linked; window run is opt-in (MCPP_RUN_WINDOW=1)"); return 0; } - -#else - -int main() { return 0; } - -#endif From 963e5e1a33af8b10163e6f4beaaad2f9cc8fca5e Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Thu, 30 Jul 2026 08:22:30 +0800 Subject: [PATCH 3/4] fix(eui-neo): pin the Windows API floor and name kernel32 for app-main MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Auditing the one TU the `app-main` feature adds — which nothing in this index compiled until tests/examples/eui-neo-app-main existed — turned up two Windows-only requirements it carries: * core/app/frame_pacing.h calls CreateWaitableTimerExW, and both mingw-w64's winbase.h and the Windows SDK guard that declaration behind `#if _WIN32_WINNT >= 0x0600`. The header sets no floor of its own, so it inherits the toolchain default, and mingw-w64 has historically defaulted as low as 0x502. Pin 0x0A00 in cflags + cxxflags rather than depend on which default the runner's llvm ships — that is effectively what upstream builds against via the MSVC SDK, and a command-line define is respected by _mingw.h's `#ifndef` guard. Every other Windows API in the package is pre-Vista, which is why this never surfaced. * the same header reaches CreateWaitableTimerExW / SetWaitableTimer / WaitForSingleObject / CloseHandle, so name -lkernel32. It is in every sane default lib set, but the surrounding comment is specifically about mcpp not inheriting CMake's CMAKE_C_STANDARD_LIBRARIES, so spelling it out is consistent and free. timeBeginPeriod (winmm) and MonitorFromWindow / GetMonitorInfoW / EnumDisplaySettingsW (user32) were already covered; glfw3native.h ships in compat.glfw's include/GLFW/. Linux is unaffected (the flags are inside the `windows` section) and all three eui-neo members re-verified green there. --- ...-30-eui-neo-window-and-app-main-members.md | 23 +++++++++++--- pkgs/e/compat.eui-neo.lua | 30 +++++++++++++++++-- 2 files changed, 46 insertions(+), 7 deletions(-) diff --git a/.agents/docs/2026-07-30-eui-neo-window-and-app-main-members.md b/.agents/docs/2026-07-30-eui-neo-window-and-app-main-members.md index 0c5f7af..4e0d32c 100644 --- a/.agents/docs/2026-07-30-eui-neo-window-and-app-main-members.md +++ b/.agents/docs/2026-07-30-eui-neo-window-and-app-main-members.md @@ -188,10 +188,25 @@ the build+link half of both members on those platforms is asserted by the `workspace (macos)` / `workspace (windows)` legs, not locally. Two things are new there and worth watching on the first run: -* `glfw_app_main.cpp` has never been compiled on any platform. Its Windows half - (``, `GLFW_EXPOSE_NATIVE_WIN32`, `glfw3native.h`, `timeBeginPeriod`, - `MonitorFromWindow`, `EnumDisplaySettingsW`) needs `-lwinmm` and `-luser32`, both - already in the descriptor's `windows.ldflags`. +* `glfw_app_main.cpp` has never been compiled on any platform, so its Windows half + is newly exercised. Two descriptor additions came out of auditing it, both + Windows-only and both for that TU alone: + + * **`-D_WIN32_WINNT=0x0A00`** (cflags + cxxflags). `core/app/frame_pacing.h` + calls `CreateWaitableTimerExW`, which mingw-w64's `winbase.h` and the Windows + SDK both guard behind `#if _WIN32_WINNT >= 0x0600`. The header sets no floor of + its own and mingw-w64 has historically defaulted as low as `0x502`, so the + build would depend on which default the runner's llvm ships. Everything else in + the package only reaches pre-Vista APIs, which is why it never came up. + * **`-lkernel32`**. `frame_pacing.h` reaches `CreateWaitableTimerExW` / + `SetWaitableTimer` / `WaitForSingleObject` / `CloseHandle`. kernel32 is in + every sane default lib set, so this is belt-and-braces — but the surrounding + comment in the descriptor is precisely about mcpp not inheriting CMake's + `CMAKE_C_STANDARD_LIBRARIES`, so naming it is consistent and free. + + `timeBeginPeriod` (winmm) and `MonitorFromWindow` / `GetMonitorInfoW` / + `EnumDisplaySettingsW` (user32) were already covered by the existing list; + `glfw3native.h` ships in compat.glfw's `include/GLFW/`. * these are the first members to link `compat.glfw` off Linux from their **own** code rather than through `compat.eui-neo`, which exercises the macOS `Cocoa`/`IOKit`/`CoreFoundation` frameworks and the Windows `-lgdi32` from a diff --git a/pkgs/e/compat.eui-neo.lua b/pkgs/e/compat.eui-neo.lua index 2537040..d7c1fc6 100644 --- a/pkgs/e/compat.eui-neo.lua +++ b/pkgs/e/compat.eui-neo.lua @@ -309,7 +309,20 @@ package = { -- through eui_neo.h), hence both lists; EUI_TRAY_WINAPI only gates -- tray_bridge.c, but keeping the pair symmetrical is cheaper than -- re-deriving which is which. - cflags = { "-DEUI_TRAY_WINAPI=1", "-DNOMINMAX" }, + -- + -- `_WIN32_WINNT` is for the `app-main` feature's TU, which nothing + -- compiled until tests/examples/eui-neo-app-main existed: + -- core/app/frame_pacing.h calls CreateWaitableTimerExW, and both + -- mingw-w64's winbase.h and the Windows SDK guard that declaration + -- behind `#if _WIN32_WINNT >= 0x0600`. The header sets no floor of + -- its own, so it inherits the toolchain default — mingw-w64 has + -- historically defaulted as low as 0x502. Pin the floor rather than + -- depend on which default the runner's llvm ships; 0x0A00 is what + -- upstream effectively builds against via the MSVC SDK, and a + -- command-line define is respected by _mingw.h's `#ifndef` guard. + -- The rest of the package only reaches pre-Vista APIs, which is why + -- this never came up before. + cflags = { "-DEUI_TRAY_WINAPI=1", "-DNOMINMAX", "-D_WIN32_WINNT=0x0A00" }, -- Upstream builds at CMAKE_CXX_STANDARD 17; this index's floor is -- c++23, and one Windows-only line does not survive the move: -- `parseWindowsSelection()` in core/platform/platform.cpp pushes @@ -326,7 +339,8 @@ package = { -- already sits eight lines above and does the right thing); until -- then this keeps us on a real upstream release tag rather than a -- fork carrying the patch. - cxxflags = { "-DEUI_TRAY_WINAPI=1", "-DNOMINMAX", "-fno-char8_t" }, + cxxflags = { "-DEUI_TRAY_WINAPI=1", "-DNOMINMAX", "-fno-char8_t", + "-D_WIN32_WINNT=0x0A00" }, -- Upstream lists winmm/urlmon/shell32/user32/imm32/pdh and stops -- there, because CMake's MSVC default `CMAKE_C_STANDARD_LIBRARIES` -- already drags in kernel32/user32/gdi32/shell32/ole32/comdlg32/… @@ -337,10 +351,20 @@ package = { -- URLDownloadToFileA and ShellExecuteA are covered by the upstream -- list.) Like the char8_t break above, this only showed up once the -- mcpp#233 collision stopped dropping the TU. + -- + -- kernel32 for the `app-main` TU: core/app/frame_pacing.h reaches + -- CreateWaitableTimerExW / SetWaitableTimer / WaitForSingleObject / + -- CloseHandle, and glfw_app_main.cpp reaches timeBeginPeriod (winmm, + -- already listed) plus MonitorFromWindow / GetMonitorInfoW / + -- EnumDisplaySettingsW (user32, already listed). kernel32 is part of + -- every sane Windows default lib set, so this is belt-and-braces for + -- the one TU in this package that had never been compiled — naming + -- it costs nothing and the comment above is precisely about mcpp not + -- inheriting CMake's defaults. ldflags = { "-lwinmm", "-lurlmon", "-lshell32", "-luser32", "-limm32", "-lpdh", "-lole32", - "-lcomdlg32", + "-lcomdlg32", "-lkernel32", }, }, From 40feb361401a23c53d5bf6794ceeaf2a0e36b25c Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Thu, 30 Jul 2026 08:36:55 +0800 Subject: [PATCH 4/4] docs(eui-neo): record the macOS/Windows CI evidence for the new members MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR #139 run 30502516910: all three workspace legs selected the same six eui-neo members and passed (linux 12m26s, macos 6m15s, windows 7m50s). The app-main assertion is self-proving and worth spelling out: the test TU defines no main(), so the binary can only run if glfw_app_main.o supplied one. Its 'linked, main() supplied by the package' line appearing on the windows and macos legs is proof that TU compiled and linked there for the first time — which is what exercises the -D_WIN32_WINNT=0x0A00 and -lkernel32 additions. --- ...-30-eui-neo-window-and-app-main-members.md | 30 ++++++++++++++++--- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/.agents/docs/2026-07-30-eui-neo-window-and-app-main-members.md b/.agents/docs/2026-07-30-eui-neo-window-and-app-main-members.md index 4e0d32c..6e18279 100644 --- a/.agents/docs/2026-07-30-eui-neo-window-and-app-main-members.md +++ b/.agents/docs/2026-07-30-eui-neo-window-and-app-main-members.md @@ -183,10 +183,32 @@ eui-neo-markdown (existing) ok `check_cross_package_refs.lua` all pass on the touched descriptor. The descriptor change is comment-only. -**macOS and Windows are CI-verified only.** There is no runner for either here, so -the build+link half of both members on those platforms is asserted by the -`workspace (macos)` / `workspace (windows)` legs, not locally. Two things are new -there and worth watching on the first run: +**macOS and Windows are CI-verified, not locally verified** — there is no runner for +either here. PR #139, run 30502516910: all three `workspace` legs selected the same +six members and passed. + +``` +selected members: eui-neo-app-main eui-neo-window eui-neo-markdown \ + eui-neo-sdl2 eui-neo-vulkan eui-neo + +workspace (linux) pass 12m26s +workspace (macos) pass 6m15s +workspace (windows) pass 7m50s +``` + +The `app-main` line on each leg is the assertion that matters, and it is +self-proving: + +``` +compat.eui-neo[app-main]: linked, main() supplied by the package; window run is opt-in +``` + +The test TU defines no `main`, so the binary can only run if `glfw_app_main.o` +supplied one. Printing that line at all therefore proves the feature's source +compiled and linked — on Windows and macOS for the first time ever, which is what +exercises the two additions below. + +Two things were new there and were the reason to watch the first run: * `glfw_app_main.cpp` has never been compiled on any platform, so its Windows half is newly exercised. Two descriptor additions came out of auditing it, both