|
| 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` 观察窗口出现、拖动无残影。 |
0 commit comments