From 26f835f4bea038144b81adcb66f32875c1ef77b0 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Thu, 4 Jun 2026 01:28:52 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20headless=20backend=20=E2=80=94=20second?= =?UTF-8?q?=20BackendApi=20implementation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds imgui.backend.headless: a display-free backend satisfying the same compile-time contract as GlfwOpenGL3 (CI smoke / logic tests / servers). The contract's lifecycle names become platform-neutral (InitPlatform/ TerminatePlatform) — exposed by having a second implementation; the Glfw-flavored spellings remain as aliases on GlfwOpenGL3. backend_swap_test runs the SAME templated application loop against Headless (executed, 3 frames render) and compile-instantiates it for GlfwOpenGL3 — proving backend swap = one import + one alias with the loop untouched (I3). --- mcpp.toml | 1 + src/backends/backend.cppm | 6 ++- src/backends/glfw_opengl3.cppm | 14 +++++- src/backends/headless.cppm | 85 ++++++++++++++++++++++++++++++++++ tests/backend_swap_test.cpp | 76 ++++++++++++++++++++++++++++++ 5 files changed, 178 insertions(+), 4 deletions(-) create mode 100644 src/backends/headless.cppm create mode 100644 tests/backend_swap_test.cpp diff --git a/mcpp.toml b/mcpp.toml index 6f54d7b..0185a9a 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -17,6 +17,7 @@ sources = [ "src/backends/platform_glfw.cppm", "src/backends/renderer_opengl3.cppm", "src/backends/glfw_opengl3.cppm", + "src/backends/headless.cppm", "src/backends/glfw_impl.cpp", "src/backends/opengl3_impl.cpp", ] diff --git a/src/backends/backend.cppm b/src/backends/backend.cppm index b043758..868fae2 100644 --- a/src/backends/backend.cppm +++ b/src/backends/backend.cppm @@ -48,8 +48,10 @@ export namespace ImGui::Backend { template concept BackendApi = requires (typename T::Window* window, ImDrawData* drawData, GlConfig config) { typename T::Window; - T::InitGlfw(); - T::TerminateGlfw(); + // Platform-neutral lifecycle names: a backend may be GLFW, SDL, or + // fully headless, so the contract must not be GLFW-flavored. + T::InitPlatform(); + T::TerminatePlatform(); T::CreateWindow(0, 0, ""); T::DestroyWindow(window); T::MakeContextCurrent(window); diff --git a/src/backends/glfw_opengl3.cppm b/src/backends/glfw_opengl3.cppm index 302c61c..a8b5be2 100644 --- a/src/backends/glfw_opengl3.cppm +++ b/src/backends/glfw_opengl3.cppm @@ -15,14 +15,24 @@ export namespace ImGui::Backend { using Window = GlfwPlatform::Window; using Monitor = GlfwPlatform::Monitor; - static bool InitGlfw() { + // Platform-neutral contract names; the Glfw-flavored spellings below + // are kept as aliases for existing consumers. + static bool InitPlatform() { return GlfwPlatform::InitGlfw(); } - static void TerminateGlfw() { + static void TerminatePlatform() { GlfwPlatform::TerminateGlfw(); } + static bool InitGlfw() { + return InitPlatform(); + } + + static void TerminateGlfw() { + TerminatePlatform(); + } + static const char* VersionString() { return GlfwPlatform::VersionString(); } diff --git a/src/backends/headless.cppm b/src/backends/headless.cppm new file mode 100644 index 0000000..ec8b2dc --- /dev/null +++ b/src/backends/headless.cppm @@ -0,0 +1,85 @@ +export module imgui.backend.headless; + +import imgui.core; +export import imgui.backend; // shared types (GlConfig/Error/FbSize) + BackendApi + +// Headless backend: a second, display-free BackendApi implementation. +// +// It satisfies the exact same compile-time contract as GlfwOpenGL3, so the +// SAME application loop runs against either backend by swapping one import +// and one alias (I3). Useful for CI smoke runs, logic tests, and servers: +// frames are produced (ImGui draw data is generated) but nothing is +// presented; RenderDrawData is a deliberate no-op. +// +// This module imports imgui.core for signatures but does NOT re-export it. +export namespace ImGui::Backend { + struct Headless { + struct WindowImpl { + int width = 0; + int height = 0; + bool shouldClose = false; + }; + using Window = WindowImpl; + + static bool InitPlatform() { return true; } + static void TerminatePlatform() {} + + static Error LastError() { return Error{}; } + + static Window* CreateWindow( + int width, + int height, + const char* /*title*/, + GlConfig /*config*/ = RecommendedGlConfig() + ) { + return new WindowImpl{width, height, false}; + } + + static void DestroyWindow(Window* window) { delete window; } + + static void MakeContextCurrent(Window* /*window*/) {} + static void SwapInterval(int /*interval*/) {} + + static FbSize FramebufferSize(Window* window) { + return window ? FbSize{window->width, window->height} : FbSize{}; + } + + static bool WindowShouldClose(Window* window) { + return window == nullptr || window->shouldClose; + } + + static void SetWindowShouldClose(Window* window, bool value) { + if (window) window->shouldClose = value; + } + + static void PollEvents() {} + static void SwapBuffers(Window* /*window*/) {} + + // ImGui bindings: a headless frame needs a display size and a built + // font atlas; there is no platform/renderer library to initialize. + static bool Init( + Window* window, + GlConfig /*config*/ = RecommendedGlConfig(), + bool /*installCallbacks*/ = true + ) { + if (window == nullptr || ImGui::GetCurrentContext() == nullptr) return false; + ImGuiIO& io = ImGui::GetIO(); + io.DisplaySize = ImVec2{static_cast(window->width), + static_cast(window->height)}; + unsigned char* pixels = nullptr; + int w = 0, h = 0; + io.Fonts->GetTexDataAsRGBA32(&pixels, &w, &h); + return pixels != nullptr; + } + + static void NewFrame() {} + static void Viewport(int, int, int, int) {} + static void ClearColor(float, float, float, float) {} + static void ClearColorBuffer() {} + static void RenderDrawData(ImDrawData* /*drawData*/) {} // headless: no-op + static void Shutdown() {} + }; + + static_assert(BackendApi, + "Headless must satisfy the backend contract"); +} diff --git a/tests/backend_swap_test.cpp b/tests/backend_swap_test.cpp new file mode 100644 index 0000000..d9c6755 --- /dev/null +++ b/tests/backend_swap_test.cpp @@ -0,0 +1,76 @@ +#include + +import imgui.core; +import imgui.backend; +import imgui.backend.headless; +import imgui.backend.glfw_opengl3; + +namespace B = ImGui::Backend; + +// Two independent implementations of the same compile-time contract. +static_assert(B::BackendApi); +static_assert(B::BackendApi); + +// The SAME application loop, parameterized only by the backend type — +// swapping backends is one import + one alias; the loop is untouched (I3). +template +int run_frames(int frames) { + if (!Backend::InitPlatform()) return -1; + auto* window = Backend::CreateWindow(320, 240, "swap-test"); + if (window == nullptr) { Backend::TerminatePlatform(); return -2; } + Backend::MakeContextCurrent(window); + + ImGuiContext* ctx = ImGui::CreateContext(); + ImGui::SetCurrentContext(ctx); + if (!Backend::Init(window)) { + ImGui::DestroyContext(ctx); + Backend::DestroyWindow(window); + Backend::TerminatePlatform(); + return -3; + } + + int rendered = 0; + for (int i = 0; i < frames && !Backend::WindowShouldClose(window); ++i) { + Backend::PollEvents(); + Backend::NewFrame(); + ImGui::NewFrame(); + ImGui::Begin("swap"); + ImGui::TextUnformatted("same loop, different backend"); + ImGui::End(); + ImGui::Render(); + + const auto fb = Backend::FramebufferSize(window); + Backend::Viewport(0, 0, fb.width, fb.height); + Backend::ClearColor(0.0f, 0.0f, 0.0f, 1.0f); + Backend::ClearColorBuffer(); + Backend::RenderDrawData(ImGui::GetDrawData()); + Backend::SwapBuffers(window); + if (ImGui::GetDrawData() != nullptr) ++rendered; + } + + Backend::Shutdown(); + ImGui::DestroyContext(ctx); + Backend::DestroyWindow(window); + Backend::TerminatePlatform(); + return rendered; +} + +// Compile-time proof the identical loop builds against the windowed backend +// too (not executed here — CI is headless). +template int run_frames(int); + +TEST(BackendSwapTest, HeadlessRunsTheSameLoop) { + EXPECT_EQ(run_frames(3), 3); +} + +TEST(BackendSwapTest, HeadlessWindowLifecycle) { + auto* w = B::Headless::CreateWindow(64, 32, "x"); + ASSERT_NE(w, nullptr); + EXPECT_FALSE(B::Headless::WindowShouldClose(w)); + B::Headless::SetWindowShouldClose(w, true); + EXPECT_TRUE(B::Headless::WindowShouldClose(w)); + const auto fb = B::Headless::FramebufferSize(w); + EXPECT_EQ(fb.width, 64); + EXPECT_EQ(fb.height, 32); + B::Headless::DestroyWindow(w); +}