Skip to content

Commit 7e241fa

Browse files
authored
Backend abstraction layer, cross-platform, unpinned toolchain (#3)
Split into imgui.core + generic backend abstraction (imgui.backend) + platform/renderer pieces composed into a single Backend type satisfying a compile-time BackendApi contract. Cross-platform (Linux/macOS/Windows), unpinned toolchain (relies on environment default). See .agents/docs/2026-06-03-imgui-backend-abstraction-design.md.
1 parent 26611c7 commit 7e241fa

18 files changed

Lines changed: 893 additions & 421 deletions

File tree

Lines changed: 307 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,307 @@
1+
# imgui-m: Backend Abstraction & Cross-Platform Design
2+
3+
> 状态: design (approved) → implementing
4+
> 分支: `codex/fix-imgui-window-clear` (PR #3)
5+
> Last updated: 2026-06-03
6+
> 目标: 把 imgui-m 拆成 `imgui.core` + 通用 backend 抽象层 + 可替换的后端实现,
7+
> 让"换后端只改 import + 一行 alias",并把工程从 Linux-only 推进到 Linux/macOS/Windows。
8+
9+
## 1. 背景与问题
10+
11+
当前结构(`0.0.1`)有三个限制:
12+
13+
1. **后端职责越界 + 组合模块大量手工转发。** `imgui.backend.glfw_opengl3`
14+
把整个子模块 API 逐函数手抄转发一遍(~每个函数一处),N 处同步漂移风险。
15+
2. **没有显式的"后端抽象层"。** 不同后端之间没有一个被编译期约束的统一表面,
16+
消费者无法做到"换后端 = 换 import"。
17+
3. **工程只验证过 Linux/x86_64。** `mcpp.toml` 只声明 `linux` toolchain,CI 只有
18+
`ubuntu-latest`;示例硬编码 `ConfigureOpenGL(3,3,true,false)`,在 macOS 上会因
19+
缺少 forward-compat + core profile 直接创建上下文失败。
20+
21+
## 2. 设计目标
22+
23+
- `imgui.core` 与 backend 彻底分层;backend 实现模块**不 re-export `imgui.core`**
24+
- 提供**通用 backend 抽象层(契约)**:用 C++20 `concept` 在编译期约束每个后端的
25+
统一 API 形状。
26+
- 每个后端对外暴露**单一 `Backend` 类型**(全 static 方法),职责包含:窗口生命周期、
27+
GL/帧缓冲操作、ImGui platform/renderer 绑定、错误诊断。
28+
- **用法一致**:`import imgui.core; import imgui.backend.<impl>;` 后,业务代码在不同
29+
后端间逐字复用,换后端只改 import 行 + 一行 `using Backend = ...`
30+
- 跨平台:Linux / macOS / Windows 都能 `mcpp build`;`RecommendedGlConfig()` 自动给出
31+
各平台正确的 GL/GLSL 配置。
32+
33+
### 非目标 (Non-Goals)
34+
35+
- 不打包 `libGL`/GLX/EGL/Mesa/驱动(沿用既有 GL runtime boundary 约定)。
36+
- 本次不实现 SDL/Vulkan/Metal/DX 后端,只**预留**扩展位。
37+
- 不追求把 Dear ImGui 全量 API 手抄进 `imgui.core`(见 §7)。
38+
- 不在 headless CI 里依赖真实显示;真实窗口运行是独立的、需显示环境的步骤。
39+
40+
## 3. 模块分层
41+
42+
```
43+
imgui.core # 纯 Dear ImGui:context / frame / widgets / draw-data
44+
imgui.backend # ★契约层:共享值类型 + BackendApi concept(无实现、不依赖平台库)
45+
46+
├─ imgui.backend.platform.glfw # 平台片:窗口/事件 + ImGui_ImplGlfw_*(可复用拼装单元)
47+
├─ imgui.backend.renderer.opengl3 # 渲染片:GL 操作 + ImGui_ImplOpenGL3_*(可复用拼装单元)
48+
49+
└─ imgui.backend.glfw_opengl3 # ★组合实现:装配 platform+renderer 为满足契约的单一 Backend 类型
50+
(预留) imgui.backend.sdl3_opengl3, imgui.backend.glfw_vulkan, ...
51+
```
52+
53+
设计理由:
54+
55+
- **契约层 (`imgui.backend`)** 是"通用 backend 抽象层"的载体:只放跨后端共享的*值类型*
56+
*概念*,不放任何实现,不 `import` 平台库。它给出统一表面的"形状定义"。
57+
- **platform / renderer 片**对应 Dear ImGui 自身的 platform/renderer 拆分,是可复用的
58+
拼装单元,避免 `GLFW×OpenGL3``GLFW×Vulkan``SDL×OpenGL3` 的组合爆炸。
59+
- **组合实现**把一个 platform 片和一个 renderer 片装配成对外的单一 `Backend` 类型,
60+
并用 `static_assert(BackendApi<...>)` 在编译期保证它符合契约。
61+
62+
### import 规则(硬约束)
63+
64+
- 所有 backend 模块**`import imgui.core`(私有,仅为签名用到 `ImDrawData` 等)**,
65+
**绝不 `export import imgui.core`**。消费者必须自己 `import imgui.core`
66+
- 但组合实现模块**`export import imgui.backend`**:`GlConfig`/`Error`/`FbSize`
67+
这些共享值类型是后端公开表面的一部分,消费者需要直接命名它们(例如接住
68+
`Backend::LastError()` 的返回值)。"不 re-export" 的约束只针对 `imgui.core`,
69+
不针对抽象层本身。
70+
- 消费者两种合法用法:
71+
- 纯逻辑 / headless:`import imgui.core;`
72+
- 带窗口:`import imgui.core; import imgui.backend.<impl>;`(两个 import 都要显式写)
73+
74+
## 4. 契约层 `imgui.backend`
75+
76+
只含跨后端共享的值类型、跨平台默认配置、以及统一 API 的 `concept`
77+
78+
```cpp
79+
export module imgui.backend;
80+
81+
import imgui.core; // 私有:仅 RenderDrawData(ImDrawData*) 等签名需要,不 re-export
82+
83+
export namespace ImGui::Backend {
84+
// ---- 共享值类型 ----
85+
struct GlConfig {
86+
int major = 3;
87+
int minor = 3;
88+
bool coreProfile = true;
89+
bool forwardCompat = false;
90+
const char* glsl = nullptr; // nullptr => 让 renderer 选默认
91+
};
92+
struct Error { int code = 0; const char* description = nullptr; };
93+
struct FbSize { int width = 0; int height = 0; };
94+
struct Rgba { float r = 0, g = 0, b = 0, a = 1; };
95+
96+
// ---- 跨平台正确默认值 ----
97+
// macOS: 强制 GL>=3.2 core + forwardCompat,glsl="#version 150"
98+
// Linux/Windows: GL 3.3 core,glsl="#version 130"/nullptr
99+
GlConfig RecommendedGlConfig();
100+
101+
// ---- 编译期契约:每个后端必须满足的统一 API 形状 ----
102+
template <class T>
103+
concept BackendApi = requires (
104+
typename T::Window* w, const char** desc, ImDrawData* dd,
105+
int i, float f, GlConfig cfg
106+
) {
107+
typename T::Window;
108+
{ T::InitGlfw() } -> std::same_as<bool>;
109+
{ T::TerminateGlfw() } -> std::same_as<void>;
110+
{ T::CreateWindow(i, i, "") } -> std::same_as<typename T::Window*>;
111+
{ T::DestroyWindow(w) } -> std::same_as<void>;
112+
{ T::MakeContextCurrent(w) } -> std::same_as<void>;
113+
{ T::Init(w, cfg) } -> std::same_as<bool>;
114+
{ T::NewFrame() } -> std::same_as<void>;
115+
{ T::Viewport(i, i, i, i) } -> std::same_as<void>;
116+
{ T::ClearColor(f, f, f, f) } -> std::same_as<void>;
117+
{ T::ClearColorBuffer() } -> std::same_as<void>;
118+
{ T::RenderDrawData(dd) } -> std::same_as<void>;
119+
{ T::SwapBuffers(w) } -> std::same_as<void>;
120+
{ T::PollEvents() } -> std::same_as<void>;
121+
{ T::WindowShouldClose(w) } -> std::same_as<bool>;
122+
{ T::FramebufferSize(w) } -> std::same_as<FbSize>;
123+
{ T::Shutdown() } -> std::same_as<void>;
124+
{ T::LastError() } -> std::same_as<Error>;
125+
};
126+
}
127+
```
128+
129+
> 说明:`concept` 约束的是"类型 T 是否具备这组 static 成员",因此每个后端用一个
130+
> `struct` 承载 API。这既给出编译期保证,又让所有后端的调用写法完全一致。
131+
132+
## 5. 后端实现:单一 `Backend` 类型
133+
134+
### 5.1 平台片 `imgui.backend.platform.glfw`
135+
136+
封装 GLFW 窗口/事件 + `ImGui_ImplGlfw_*`,导出一个 `struct GlfwPlatform`(全 static)。
137+
内部使用 `#define GLFW_INCLUDE_NONE` + `<GLFW/glfw3.h>` + `<imgui_impl_glfw.h>`。
138+
负责:`InitGlfw/Terminate/CreateWindow/.../FramebufferSize/LastError` 与 ImGui 平台帧。
139+
GL 上下文创建按传入 `GlConfig` 设置 window hints(含 macOS forward-compat)。
140+
141+
### 5.2 渲染片 `imgui.backend.renderer.opengl3`
142+
143+
封装 `<imgui_impl_opengl3.h>` + loader,导出 `struct OpenGL3Renderer`(全 static):
144+
`Init(glsl)/Shutdown/NewFrame/Viewport/ClearColor/ClearColorBuffer/RenderDrawData`。
145+
146+
### 5.3 组合实现 `imgui.backend.glfw_opengl3`
147+
148+
```cpp
149+
export module imgui.backend.glfw_opengl3;
150+
151+
import imgui.core; // 私有,不 re-export
152+
export import imgui.backend; // 契约层类型(GlConfig/Error/FbSize)随后端公开
153+
import imgui.backend.platform.glfw; // 私有装配单元
154+
import imgui.backend.renderer.opengl3; // 私有装配单元
155+
156+
export namespace ImGui::Backend {
157+
struct GlfwOpenGL3 {
158+
using Window = GlfwPlatform::Window;
159+
using Monitor = GlfwPlatform::Monitor;
160+
161+
static bool InitGlfw() { return GlfwPlatform::InitGlfw(); }
162+
static void TerminateGlfw() { GlfwPlatform::TerminateGlfw(); }
163+
static Window* CreateWindow(int w, int h, const char* t,
164+
Monitor* m = nullptr, Window* s = nullptr);
165+
static bool Init(Window* w, GlConfig cfg = RecommendedGlConfig());
166+
static void NewFrame(); // renderer + platform 帧
167+
static void Viewport(int, int, int, int);
168+
static void ClearColor(float, float, float, float);
169+
static void ClearColorBuffer();
170+
static void RenderDrawData(ImDrawData*);
171+
static void SwapBuffers(Window*);
172+
static void PollEvents();
173+
static bool WindowShouldClose(Window*);
174+
static FbSize FramebufferSize(Window*);
175+
static void Shutdown(); // renderer + platform 关闭
176+
static Error LastError();
177+
// ... 其余诊断/回调按需
178+
};
179+
180+
static_assert(BackendApi<GlfwOpenGL3>); // 编译期保证用法一致
181+
}
182+
```
183+
184+
要点:
185+
- **取消逐函数手工转发的"组合模块"形态** —— 现在是单一类型、单处定义,内部委托
186+
platform/renderer 片。
187+
- `Init` 内部:`GlfwPlatform::InitForOpenGL(...)` 失败即返回;再 `OpenGL3Renderer::Init(cfg.glsl)`,
188+
失败则回滚 platform 关闭(保留既有错误回滚语义)。
189+
- **不隐式清屏**:`RenderDrawData` 只画 ImGui,清屏交给应用(`ClearColor`/`ClearColorBuffer`),
190+
以支持"先渲染自己的场景、ImGui 作 overlay"——这正是 PR #3 的设计取向。
191+
192+
## 6. 用法一致(消费者视角)
193+
194+
```cpp
195+
import imgui.core;
196+
import imgui.backend.glfw_opengl3;
197+
using Backend = ImGui::Backend::GlfwOpenGL3; // ← 换后端只改这两行(import + alias)
198+
199+
int main() {
200+
if (!Backend::InitGlfw()) { /* Backend::LastError() */ return 1; }
201+
auto* window = Backend::CreateWindow(960, 540, "demo");
202+
Backend::MakeContextCurrent(window);
203+
204+
auto* ctx = ImGui::CreateContext();
205+
ImGui::SetCurrentContext(ctx);
206+
Backend::Init(window); // 默认 RecommendedGlConfig(),跨平台正确
207+
208+
while (!Backend::WindowShouldClose(window)) {
209+
Backend::PollEvents();
210+
Backend::NewFrame();
211+
ImGui::NewFrame();
212+
ImGui::Begin("hello"); ImGui::TextUnformatted("..."); ImGui::End();
213+
ImGui::Render();
214+
215+
auto fb = Backend::FramebufferSize(window);
216+
Backend::Viewport(0, 0, fb.width, fb.height);
217+
Backend::ClearColor(0.08f, 0.09f, 0.10f, 1.0f);
218+
Backend::ClearColorBuffer();
219+
Backend::RenderDrawData(ImGui::GetDrawData());
220+
Backend::SwapBuffers(window);
221+
}
222+
Backend::Shutdown();
223+
ImGui::DestroyContext(ctx);
224+
Backend::DestroyWindow(window);
225+
Backend::TerminateGlfw();
226+
}
227+
```
228+
229+
换成将来的 `imgui.backend.sdl3_opengl3` 时,业务循环逐字不变;`concept` +
230+
`static_assert` 保证两个后端表面完全一致。
231+
232+
## 7. `imgui.core` 导出策略(可扩展性)
233+
234+
当前 `core.cppm` 是手维护的 `using` 白名单,每加一个 ImGui 函数都要补一行,不可持续。
235+
本设计确立约定:
236+
237+
- `imgui.core` 导出**常用核心子集**(context 生命周期、frame、常用 widgets、draw-data、
238+
基础类型 `ImVec2/ImVec4/ImGuiContext/ImGuiIO/ImFontAtlas/ImDrawData`)。
239+
- 对暂未导出的冷门 API,提供**显式逃生舱口**:文档约定消费者可在自己的 TU 里
240+
`#include <imgui.h>`(global module fragment)直接调用,与模块用法并存。
241+
- 不在本次扩成全量;后续按需增量补充导出子集。
242+
243+
> 本次实现范围:在 §3 分层与 §4–§6 落地的前提下,`imgui.core` 至少补齐 `ImVec4` 与
244+
> 示例所需 widgets;全量导出策略留作后续。
245+
246+
## 8. 跨平台工程化
247+
248+
### 8.1 toolchain
249+
250+
`mcpp.toml` 增加 macOS / Windows toolchain 条目(与 mcpp 支持的工具链命名对齐),
251+
例如:
252+
253+
```toml
254+
[toolchain]
255+
linux = "llvm@20.1.7"
256+
macos = "llvm@20.1.7" # 或系统 clang;最终以 mcpp-index 可用项为准
257+
windows = "llvm@20.1.7"
258+
```
259+
260+
### 8.2 GL/GLSL 配置
261+
262+
`RecommendedGlConfig()` 按平台返回:
263+
264+
| 平台 | major.minor | coreProfile | forwardCompat | glsl |
265+
|---|---|---|---|---|
266+
| Linux | 3.3 | true | false | `#version 130` |
267+
| Windows | 3.3 | true | false | `#version 130` |
268+
| macOS | 3.2 | true | **true** | `#version 150` |
269+
270+
示例改用 `Backend::Init(window)`(默认即 `RecommendedGlConfig()`),**移除硬编码**
271+
`ConfigureOpenGL(3,3,true,false)`
272+
273+
### 8.3 CI 矩阵
274+
275+
`.github/workflows/ci.yml` 由单 `ubuntu-latest` 扩为 `ubuntu-latest` /
276+
`macos-latest` / `windows-latest` 的 build-check 矩阵:
277+
278+
- 每个 runner:`mcpp build` + `mcpp test` + build 三个 examples。
279+
- headless 例子 `examples/basic` 可在所有平台 `mcpp run`
280+
- **真实窗口运行**保持为需显示环境的条件步骤(不进 headless CI 必经路径)。
281+
282+
## 9. 影响面 / 文件清单
283+
284+
新增:
285+
- `src/backends/backend.cppm` (契约层)
286+
- `src/backends/platform_glfw.cppm` (+ 既有 `glfw_impl.cpp`)
287+
- `src/backends/renderer_opengl3.cppm` (+ 既有 `opengl3_impl.cpp`)
288+
289+
改造:
290+
- `src/backends/glfw_opengl3.cppm` → 单一 `GlfwOpenGL3` 类型
291+
- `src/core.cppm` → 补 `ImVec4` 等(§7)
292+
- `tests/backend_test.cpp` → 针对新表面 + `static_assert(BackendApi<...>)`
293+
- `examples/minimal_window``examples/glfw_opengl3` → 统一 `Backend::` 用法 + `RecommendedGlConfig()`
294+
- `mcpp.toml`(toolchain + sources)、`.github/workflows/ci.yml`(矩阵)
295+
- `docs/architecture.md``README.md` 同步
296+
297+
> 旧的 `imgui.backend.glfw` / `imgui.backend.opengl3` 自由函数命名空间被
298+
> platform/renderer 片取代。如需保留兼容名,可在过渡期保留薄别名;本次按"干净切换"处理。
299+
300+
## 10. 验证
301+
302+
- `mcpp build` / `mcpp test`(三平台 CI)
303+
- `cd examples/basic && mcpp run`(headless,跨平台)
304+
- `cd examples/minimal_window && mcpp build`
305+
- `cd examples/glfw_opengl3 && mcpp build`
306+
- **真实窗口运行**(本机 DISPLAY + OpenGL runtime 可用时):
307+
`cd examples/minimal_window && mcpp run` 观察窗口出现、拖动无残影。

.github/workflows/ci.yml

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,44 +16,77 @@ concurrency:
1616

1717
jobs:
1818
mcpp:
19-
runs-on: ubuntu-latest
20-
timeout-minutes: 30
19+
name: mcpp (${{ matrix.os }})
20+
strategy:
21+
fail-fast: false
22+
matrix:
23+
include:
24+
- os: ubuntu-latest
25+
experimental: false
26+
- os: macos-latest
27+
experimental: true
28+
- os: windows-latest
29+
experimental: true
30+
runs-on: ${{ matrix.os }}
31+
# Linux is the required, must-pass platform. macOS/Windows are best-effort
32+
# while mcpp-index runtime/toolchain support for them settles, so they do
33+
# not block the overall CI result.
34+
continue-on-error: ${{ matrix.experimental }}
35+
timeout-minutes: 45
2136
steps:
2237
- name: Checkout
2338
uses: actions/checkout@v4
2439

25-
- name: Install Xlings
40+
- name: Install Xlings (Linux/macOS)
41+
if: runner.os != 'Windows'
2642
env:
2743
XLINGS_NON_INTERACTIVE: 1
44+
shell: bash
2845
run: |
2946
curl -fsSL https://raw.githubusercontent.com/openxlings/xlings/refs/heads/main/tools/other/quick_install.sh | bash
3047
echo "$HOME/.xlings/subos/current/bin" >> "$GITHUB_PATH"
3148
49+
- name: Install Xlings (Windows)
50+
if: runner.os == 'Windows'
51+
env:
52+
XLINGS_NON_INTERACTIVE: 1
53+
shell: pwsh
54+
run: |
55+
irm https://raw.githubusercontent.com/openxlings/xlings/main/tools/other/quick_install.ps1 | iex
56+
"$env:USERPROFILE\.xlings\subos\current\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
57+
3258
- name: Install project tools
59+
shell: bash
3360
run: |
3461
xlings update
3562
xlings install
3663
3764
- name: Show mcpp version
65+
shell: bash
3866
run: mcpp --version
3967

4068
- name: Build library
69+
shell: bash
4170
run: mcpp build
4271

4372
- name: Run tests
73+
shell: bash
4474
run: mcpp test
4575

4676
- name: Run headless example
77+
shell: bash
4778
run: |
4879
cd examples/basic
4980
mcpp run
5081
5182
- name: Build minimal window example
83+
shell: bash
5284
run: |
5385
cd examples/minimal_window
5486
mcpp build
5587
5688
- name: Build GLFW/OpenGL3 example
89+
shell: bash
5790
run: |
5891
cd examples/glfw_opengl3
5992
mcpp build

0 commit comments

Comments
 (0)