Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions mcpp.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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",
]
Expand Down
6 changes: 4 additions & 2 deletions src/backends/backend.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,10 @@ export namespace ImGui::Backend {
template <class T>
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);
Expand Down
14 changes: 12 additions & 2 deletions src/backends/glfw_opengl3.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
Expand Down
85 changes: 85 additions & 0 deletions src/backends/headless.cppm
Original file line number Diff line number Diff line change
@@ -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<float>(window->width),
static_cast<float>(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>,
"Headless must satisfy the backend contract");
}
76 changes: 76 additions & 0 deletions tests/backend_swap_test.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
#include <gtest/gtest.h>

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<B::Headless>);
static_assert(B::BackendApi<B::GlfwOpenGL3>);

// The SAME application loop, parameterized only by the backend type —
// swapping backends is one import + one alias; the loop is untouched (I3).
template <class Backend>
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<B::GlfwOpenGL3>(int);

TEST(BackendSwapTest, HeadlessRunsTheSameLoop) {
EXPECT_EQ(run_frames<B::Headless>(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);
}
Loading