Skip to content

Commit ad375b3

Browse files
feat: headless backend — second BackendApi implementation (#5)
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). Co-authored-by: sunrisepeak <x.d2learn.org@gmail.com>
1 parent c53e197 commit ad375b3

5 files changed

Lines changed: 178 additions & 4 deletions

File tree

mcpp.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ sources = [
1717
"src/backends/platform_glfw.cppm",
1818
"src/backends/renderer_opengl3.cppm",
1919
"src/backends/glfw_opengl3.cppm",
20+
"src/backends/headless.cppm",
2021
"src/backends/glfw_impl.cpp",
2122
"src/backends/opengl3_impl.cpp",
2223
]

src/backends/backend.cppm

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,10 @@ export namespace ImGui::Backend {
4848
template <class T>
4949
concept BackendApi = requires (typename T::Window* window, ImDrawData* drawData, GlConfig config) {
5050
typename T::Window;
51-
T::InitGlfw();
52-
T::TerminateGlfw();
51+
// Platform-neutral lifecycle names: a backend may be GLFW, SDL, or
52+
// fully headless, so the contract must not be GLFW-flavored.
53+
T::InitPlatform();
54+
T::TerminatePlatform();
5355
T::CreateWindow(0, 0, "");
5456
T::DestroyWindow(window);
5557
T::MakeContextCurrent(window);

src/backends/glfw_opengl3.cppm

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,24 @@ export namespace ImGui::Backend {
1515
using Window = GlfwPlatform::Window;
1616
using Monitor = GlfwPlatform::Monitor;
1717

18-
static bool InitGlfw() {
18+
// Platform-neutral contract names; the Glfw-flavored spellings below
19+
// are kept as aliases for existing consumers.
20+
static bool InitPlatform() {
1921
return GlfwPlatform::InitGlfw();
2022
}
2123

22-
static void TerminateGlfw() {
24+
static void TerminatePlatform() {
2325
GlfwPlatform::TerminateGlfw();
2426
}
2527

28+
static bool InitGlfw() {
29+
return InitPlatform();
30+
}
31+
32+
static void TerminateGlfw() {
33+
TerminatePlatform();
34+
}
35+
2636
static const char* VersionString() {
2737
return GlfwPlatform::VersionString();
2838
}

src/backends/headless.cppm

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
export module imgui.backend.headless;
2+
3+
import imgui.core;
4+
export import imgui.backend; // shared types (GlConfig/Error/FbSize) + BackendApi
5+
6+
// Headless backend: a second, display-free BackendApi implementation.
7+
//
8+
// It satisfies the exact same compile-time contract as GlfwOpenGL3, so the
9+
// SAME application loop runs against either backend by swapping one import
10+
// and one alias (I3). Useful for CI smoke runs, logic tests, and servers:
11+
// frames are produced (ImGui draw data is generated) but nothing is
12+
// presented; RenderDrawData is a deliberate no-op.
13+
//
14+
// This module imports imgui.core for signatures but does NOT re-export it.
15+
export namespace ImGui::Backend {
16+
struct Headless {
17+
struct WindowImpl {
18+
int width = 0;
19+
int height = 0;
20+
bool shouldClose = false;
21+
};
22+
using Window = WindowImpl;
23+
24+
static bool InitPlatform() { return true; }
25+
static void TerminatePlatform() {}
26+
27+
static Error LastError() { return Error{}; }
28+
29+
static Window* CreateWindow(
30+
int width,
31+
int height,
32+
const char* /*title*/,
33+
GlConfig /*config*/ = RecommendedGlConfig()
34+
) {
35+
return new WindowImpl{width, height, false};
36+
}
37+
38+
static void DestroyWindow(Window* window) { delete window; }
39+
40+
static void MakeContextCurrent(Window* /*window*/) {}
41+
static void SwapInterval(int /*interval*/) {}
42+
43+
static FbSize FramebufferSize(Window* window) {
44+
return window ? FbSize{window->width, window->height} : FbSize{};
45+
}
46+
47+
static bool WindowShouldClose(Window* window) {
48+
return window == nullptr || window->shouldClose;
49+
}
50+
51+
static void SetWindowShouldClose(Window* window, bool value) {
52+
if (window) window->shouldClose = value;
53+
}
54+
55+
static void PollEvents() {}
56+
static void SwapBuffers(Window* /*window*/) {}
57+
58+
// ImGui bindings: a headless frame needs a display size and a built
59+
// font atlas; there is no platform/renderer library to initialize.
60+
static bool Init(
61+
Window* window,
62+
GlConfig /*config*/ = RecommendedGlConfig(),
63+
bool /*installCallbacks*/ = true
64+
) {
65+
if (window == nullptr || ImGui::GetCurrentContext() == nullptr) return false;
66+
ImGuiIO& io = ImGui::GetIO();
67+
io.DisplaySize = ImVec2{static_cast<float>(window->width),
68+
static_cast<float>(window->height)};
69+
unsigned char* pixels = nullptr;
70+
int w = 0, h = 0;
71+
io.Fonts->GetTexDataAsRGBA32(&pixels, &w, &h);
72+
return pixels != nullptr;
73+
}
74+
75+
static void NewFrame() {}
76+
static void Viewport(int, int, int, int) {}
77+
static void ClearColor(float, float, float, float) {}
78+
static void ClearColorBuffer() {}
79+
static void RenderDrawData(ImDrawData* /*drawData*/) {} // headless: no-op
80+
static void Shutdown() {}
81+
};
82+
83+
static_assert(BackendApi<Headless>,
84+
"Headless must satisfy the backend contract");
85+
}

tests/backend_swap_test.cpp

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
#include <gtest/gtest.h>
2+
3+
import imgui.core;
4+
import imgui.backend;
5+
import imgui.backend.headless;
6+
import imgui.backend.glfw_opengl3;
7+
8+
namespace B = ImGui::Backend;
9+
10+
// Two independent implementations of the same compile-time contract.
11+
static_assert(B::BackendApi<B::Headless>);
12+
static_assert(B::BackendApi<B::GlfwOpenGL3>);
13+
14+
// The SAME application loop, parameterized only by the backend type —
15+
// swapping backends is one import + one alias; the loop is untouched (I3).
16+
template <class Backend>
17+
int run_frames(int frames) {
18+
if (!Backend::InitPlatform()) return -1;
19+
auto* window = Backend::CreateWindow(320, 240, "swap-test");
20+
if (window == nullptr) { Backend::TerminatePlatform(); return -2; }
21+
Backend::MakeContextCurrent(window);
22+
23+
ImGuiContext* ctx = ImGui::CreateContext();
24+
ImGui::SetCurrentContext(ctx);
25+
if (!Backend::Init(window)) {
26+
ImGui::DestroyContext(ctx);
27+
Backend::DestroyWindow(window);
28+
Backend::TerminatePlatform();
29+
return -3;
30+
}
31+
32+
int rendered = 0;
33+
for (int i = 0; i < frames && !Backend::WindowShouldClose(window); ++i) {
34+
Backend::PollEvents();
35+
Backend::NewFrame();
36+
ImGui::NewFrame();
37+
ImGui::Begin("swap");
38+
ImGui::TextUnformatted("same loop, different backend");
39+
ImGui::End();
40+
ImGui::Render();
41+
42+
const auto fb = Backend::FramebufferSize(window);
43+
Backend::Viewport(0, 0, fb.width, fb.height);
44+
Backend::ClearColor(0.0f, 0.0f, 0.0f, 1.0f);
45+
Backend::ClearColorBuffer();
46+
Backend::RenderDrawData(ImGui::GetDrawData());
47+
Backend::SwapBuffers(window);
48+
if (ImGui::GetDrawData() != nullptr) ++rendered;
49+
}
50+
51+
Backend::Shutdown();
52+
ImGui::DestroyContext(ctx);
53+
Backend::DestroyWindow(window);
54+
Backend::TerminatePlatform();
55+
return rendered;
56+
}
57+
58+
// Compile-time proof the identical loop builds against the windowed backend
59+
// too (not executed here — CI is headless).
60+
template int run_frames<B::GlfwOpenGL3>(int);
61+
62+
TEST(BackendSwapTest, HeadlessRunsTheSameLoop) {
63+
EXPECT_EQ(run_frames<B::Headless>(3), 3);
64+
}
65+
66+
TEST(BackendSwapTest, HeadlessWindowLifecycle) {
67+
auto* w = B::Headless::CreateWindow(64, 32, "x");
68+
ASSERT_NE(w, nullptr);
69+
EXPECT_FALSE(B::Headless::WindowShouldClose(w));
70+
B::Headless::SetWindowShouldClose(w, true);
71+
EXPECT_TRUE(B::Headless::WindowShouldClose(w));
72+
const auto fb = B::Headless::FramebufferSize(w);
73+
EXPECT_EQ(fb.width, 64);
74+
EXPECT_EQ(fb.height, 32);
75+
B::Headless::DestroyWindow(w);
76+
}

0 commit comments

Comments
 (0)