Skip to content

Commit 7ec2725

Browse files
committed
refactor: render ImGui through CNA
1 parent c4e4b70 commit 7ec2725

19 files changed

Lines changed: 522 additions & 137 deletions

CMakeLists.txt

Lines changed: 34 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -73,26 +73,20 @@ endfunction()
7373
# Graphics backend selection
7474
# -----------------------------------------------------------------------------
7575
#
76-
# This variable selects the CNA *engine* backend. IMPORTANT: it is NOT the
77-
# MeshCraft editor GUI backend. The editor's ImGui layer is hard-wired to
78-
# OpenGL/GLES3 (ImGui_ImplOpenGL3 + ImGui_ImplSDL3_InitForOpenGL +
79-
# SDL_GL_GetCurrentContext, see src/MeshCraft/MeshCraftApplication.cpp), so the
80-
# GUI editor only renders under a GL context — i.e. EASYGL (desktop GL) or its
81-
# Emscripten WebGL2 variant. SDL_RENDERER / BGFX / VULKAN configure CNA's engine
82-
# but leave the editor UI unable to render (no matching ImGui backend, no GL
83-
# context). Use them only for CNA-level experimentation or the CNA-free CLI
84-
# tools (mc3togltf / mc3tomcb), which don't depend on the backend at all.
76+
# This variable selects the CNA engine backend. MeshCraft's ImGui renderer
77+
# consumes ImDrawData through CNA graphics APIs, so it does not require an
78+
# native graphics context or a renderer backend tied to one API.
8579
#
8680
# Override with:
87-
# cmake -S . -B build -DMESH_CRAFT_GRAPHICS_BACKEND=EASYGL # editor GUI works
88-
# cmake -S . -B build -DMESH_CRAFT_GRAPHICS_BACKEND=SDL_RENDERER # CNA engine only
81+
# cmake -S . -B build -DMESH_CRAFT_GRAPHICS_BACKEND=EASYGL
82+
# cmake -S . -B build-vulkan -DMESH_CRAFT_GRAPHICS_BACKEND=VULKAN
8983
#
9084
# Accepted (CNA engine): SDL_RENDERER, EASYGL, BGFX, VULKAN.
91-
# Functional for the MeshCraft editor GUI: EASYGL only.
85+
# Editor compatibility is determined by CNA's selected backend capabilities.
9286
# -----------------------------------------------------------------------------
9387

9488
set(MESH_CRAFT_GRAPHICS_BACKEND "EASYGL" CACHE STRING
95-
"CNA engine backend. The editor GUI only renders on EASYGL.")
89+
"CNA engine backend used by the MeshCraft editor GUI.")
9690
set_property(CACHE MESH_CRAFT_GRAPHICS_BACKEND PROPERTY STRINGS
9791
SDL_RENDERER EASYGL BGFX VULKAN)
9892

@@ -117,21 +111,6 @@ endif()
117111

118112
message(STATUS "MeshCraft graphics backend (CNA engine): ${MESH_CRAFT_GRAPHICS_BACKEND_UPPER}")
119113

120-
# Gate C truthfulness: the editor GUI only renders on EASYGL. Warn loudly for any
121-
# other backend so a non-functional editor build is never produced silently. The
122-
# CNA-free CLI tools are unaffected. Set MESH_CRAFT_SILENCE_BACKEND_WARNING=ON to
123-
# suppress this when you are deliberately building CNA-only / a CLI-only target.
124-
if(NOT MESH_CRAFT_GRAPHICS_BACKEND_UPPER STREQUAL "EASYGL"
125-
AND NOT MESH_CRAFT_SILENCE_BACKEND_WARNING)
126-
message(WARNING
127-
"MESH_CRAFT_GRAPHICS_BACKEND='${MESH_CRAFT_GRAPHICS_BACKEND_UPPER}' selects the "
128-
"CNA engine backend, but the MeshCraft *editor GUI* is hard-wired to OpenGL/GLES3 "
129-
"(ImGui_ImplOpenGL3). Under this backend the editor will build but its UI will NOT "
130-
"render (no GL context / no matching ImGui backend). Use EASYGL for a working editor; "
131-
"the CNA-free CLI tools (mc3togltf, mc3tomcb) work under any backend. "
132-
"Set -DMESH_CRAFT_SILENCE_BACKEND_WARNING=ON to silence this.")
133-
endif()
134-
135114
if(MESH_CRAFT_GRAPHICS_BACKEND_UPPER STREQUAL "SDL_RENDERER")
136115
set(CNA_BACKEND_SDL_RENDERER ON CACHE BOOL "" FORCE)
137116
set(CNA_BACKEND_EASY_GL OFF CACHE BOOL "" FORCE)
@@ -248,7 +227,7 @@ add_subdirectory(mcb)
248227
# mc3tomcb — CLI converter between MC3 XML and MCB binary
249228
add_subdirectory(mc3tomcb)
250229

251-
# ImGui — immediate-mode UI (SDL3 + OpenGL ES 3 backends)
230+
# ImGui — immediate-mode UI (SDL3 input platform backend; rendering is CNA).
252231
include(FetchContent)
253232
FetchContent_Declare(imgui
254233
GIT_REPOSITORY https://github.com/ocornut/imgui.git
@@ -263,13 +242,11 @@ add_library(imgui STATIC
263242
${imgui_SOURCE_DIR}/imgui_tables.cpp
264243
${imgui_SOURCE_DIR}/imgui_widgets.cpp
265244
${imgui_SOURCE_DIR}/backends/imgui_impl_sdl3.cpp
266-
${imgui_SOURCE_DIR}/backends/imgui_impl_opengl3.cpp
267245
)
268246
target_include_directories(imgui PUBLIC
269247
${imgui_SOURCE_DIR}
270248
${imgui_SOURCE_DIR}/backends
271249
)
272-
target_compile_definitions(imgui PUBLIC IMGUI_IMPL_OPENGL_ES3)
273250

274251
# SDL3 headers: use prebuilt path from CNA's SDL3_DIR cache variable
275252
# SDL3_DIR is .../lib/cmake/SDL3, so include dir is 3 levels up + /include
@@ -279,9 +256,6 @@ if(DEFINED SDL3_DIR)
279256
elseif(TARGET SDL3::SDL3)
280257
target_link_libraries(imgui PUBLIC SDL3::SDL3)
281258
endif()
282-
if(NOT EMSCRIPTEN)
283-
target_link_libraries(imgui PUBLIC GLESv2)
284-
endif()
285259

286260
# Manifold — CSG boolean mesh evaluation. Pinned at v3.0.0 (STAB-0222); no
287261
# minimum-version requirement beyond this exact pin has been established —
@@ -493,7 +467,7 @@ meshcraft_apply_sanitize(${_target})
493467
# runtime check is the surgical alternative that still satisfies Gate C
494468
# ("unsupported and rejected clearly"): embed the selected backend name, and
495469
# main() (src/MeshCraft/main.cpp) refuses to proceed under a backend the
496-
# editor cannot render on, before any window/GL initialization -- an
470+
# editor cannot render on, before any window or graphics-device initialization -- an
497471
# unsupported configuration is REJECTED at startup with a clear message and a
498472
# non-zero exit code, not silently launched as a broken window. The CLI tools
499473
# (mc3togltf/mc3tomcb) do not link this target and are unaffected.
@@ -911,6 +885,22 @@ if(MESH_CRAFT_BUILD_TESTING AND UNIX AND NOT ANDROID AND NOT EMSCRIPTEN)
911885
add_test(NAME graphics_backend_check COMMAND graphics_backend_check_test)
912886
set_tests_properties(graphics_backend_check PROPERTIES TIMEOUT 15 LABELS "unit")
913887

888+
add_executable(imgui_texture_registry_test
889+
${CMAKE_CURRENT_SOURCE_DIR}/test/imgui_texture_registry_test.cpp)
890+
target_include_directories(imgui_texture_registry_test PRIVATE
891+
${CMAKE_CURRENT_SOURCE_DIR}/include)
892+
target_compile_features(imgui_texture_registry_test PRIVATE cxx_std_23)
893+
add_test(NAME imgui_texture_registry COMMAND imgui_texture_registry_test)
894+
set_tests_properties(imgui_texture_registry PROPERTIES TIMEOUT 15 LABELS "unit")
895+
896+
add_executable(imgui_draw_translation_test
897+
${CMAKE_CURRENT_SOURCE_DIR}/test/imgui_draw_translation_test.cpp)
898+
target_include_directories(imgui_draw_translation_test PRIVATE
899+
${CMAKE_CURRENT_SOURCE_DIR}/include)
900+
target_compile_features(imgui_draw_translation_test PRIVATE cxx_std_23)
901+
add_test(NAME imgui_draw_translation COMMAND imgui_draw_translation_test)
902+
set_tests_properties(imgui_draw_translation PROPERTIES TIMEOUT 15 LABELS "unit")
903+
914904
# SYS-W7-02: differential geometry tests -- viewport renderer (via the
915905
# CNA-free tessellateUnit*Alg() mirror in
916906
# include/MeshCraft/Renderer/PrimitiveTessellationAlg.hpp) vs. the glTF
@@ -1082,6 +1072,15 @@ if(MESH_CRAFT_BUILD_TESTING AND UNIX AND NOT ANDROID AND NOT EMSCRIPTEN)
10821072
"${CMAKE_CURRENT_SOURCE_DIR}")
10831073
set_tests_properties(undo_snapshot_lint PROPERTIES TIMEOUT 15 LABELS "lint")
10841074

1075+
# SYS-W8-02/03/04: production ImGui rendering must stay on CNA's
1076+
# public graphics API; a native OpenGL backend/texture handle would
1077+
# make alternate CNA backends impossible again.
1078+
add_test(NAME imgui_renderer_portability
1079+
COMMAND "${PYTHON3_FOR_LINT}"
1080+
"${CMAKE_CURRENT_SOURCE_DIR}/test/imgui_renderer_portability_test.py"
1081+
"${CMAKE_CURRENT_SOURCE_DIR}")
1082+
set_tests_properties(imgui_renderer_portability PROPERTIES TIMEOUT 15 LABELS "lint")
1083+
10851084
# plan.md/NEXT.md mechanical self-consistency: unique task IDs, DONE
10861085
# counts match the summary line, DONE tasks cite a commit, the priority
10871086
# queue never points at a DONE task, no machine-specific absolute paths.

NEXT.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -78,9 +78,9 @@ before when explicitly requested (`SYS-W14-##` rows).
7878
## 2. Current status
7979

8080
- **Build: clean**, `cmake --build b-release -j4` passed after SVG texture
81-
support was added (EASYGL backend on Linux — the only backend buildable
82-
here).
83-
- **Tests:** 177 tests are registered. SVG-specific verification passes with
81+
support was added (EASYGL backend on Linux). The CNA-backed ImGui adapter
82+
also builds there; alternate-backend runtime qualification remains blocked.
83+
- **Tests:** 180 tests are registered. SVG-specific verification passes with
8484
`-j4`: external and inline SVG export to glTF PNGs, bounded/malformed input,
8585
cache invalidation, and real headless viewport screenshots sampling the
8686
rasterized material pixels. This session's own
@@ -90,10 +90,10 @@ before when explicitly requested (`SYS-W14-##` rows).
9090
count this file last recorded (2026-07-19) predates unrelated work not
9191
narrated here (a further audit pass and the `SYS-W14-18..27`
9292
mc3-format-vs-editor gap closures — see `plan.md`/memory, not fully
93-
reflected in this file's own history) — don't treat 142→177 as this
93+
reflected in this file's own history) — don't treat 142→180 as this
9494
session's own delta.
95-
All builds/tests this session used `-j4` (not `-j$(nproc)`), per the
96-
user's standing request (shared machine).
95+
All builds/tests this session used at most `-j4` (never `-j$(nproc)`), per
96+
the user's standing request (shared machine).
9797
- **CLI/tools/apps/libraries currently available:**
9898
- `MeshCraft` — the interactive editor (`./b-release/MeshCraft
9999
scene.mc3.xml`, or `--screenshot out.png` / `--export out.glb` /

TESTING.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
_Last updated: 2026-07-25. Counts below were produced from the live release build with `ctest -L <label> -N`, not carried over from an earlier revision. This document is derived from the actual `CMakeLists.txt` test registrations and test source files — if it drifts from `ctest -N`'s output, trust `ctest -N`, not this file's claimed count._
44

5-
MeshCraft's tests run through **CTest****177 tests registered** (`ctest --print-labels` label breakdown: `ai` 1, `commands` 1, `export` 71, `format` 37, `lint` 3, `perf` 2, `registry` 1, `render` 34, `unit` 29), mixing C++ assertion-based binaries and Python/bash subprocess-driven checks against the `mc3togltf`/`mc3tomcb` CLIs and the `MeshCraft` editor binary itself (headless `--screenshot` real-pixel-sampling tests, plus 3 tests that drive real headless Blender for GLB-import verification). `render_display_preflight` verifies an actual `xvfb-run` + `xdpyinfo` X client before the render subset; if unavailable, it reports a CTest skip and CMake disables only the other render-labelled tests. **Known gap:** the "CLI-driving Python tests" table below documents the most significant/representative tests in each category but is not exhaustively 1:1 with all registrations — `ctest -N` and `ctest --print-labels` are authoritative for the complete list.
5+
MeshCraft's tests run through **CTest****180 tests registered** (`ctest --print-labels` label breakdown: `ai` 1, `commands` 1, `export` 71, `format` 37, `lint` 4, `perf` 2, `registry` 1, `render` 34, `unit` 31), mixing C++ assertion-based binaries and Python/bash subprocess-driven checks against the `mc3togltf`/`mc3tomcb` CLIs and the `MeshCraft` editor binary itself (headless `--screenshot` real-pixel-sampling tests, plus 3 tests that drive real headless Blender for GLB-import verification). `render_display_preflight` verifies an actual `xvfb-run` + `xdpyinfo` X client before the render subset; if unavailable, it reports a CTest skip and CMake disables only the other render-labelled tests. **Known gap:** the "CLI-driving Python tests" table below documents the most significant/representative tests in each category but is not exhaustively 1:1 with all registrations — `ctest -N` and `ctest --print-labels` are authoritative for the complete list.
66

77
---
88

@@ -31,7 +31,7 @@ ctest --print-labels # list all labels
3131
ctest --rerun-failed --output-on-failure
3232
```
3333

34-
Expected result: every registered test passes. The 177-registration count was checked 2026-07-25; the relevant focused suites are run with `-j4` after each change. A failing test prints its assertion/subprocess output inline with `--output-on-failure`; without that flag, CTest only shows pass/fail per test name.
34+
Expected result: every registered test passes. The 180-registration count was checked 2026-07-25; the relevant focused suites are run with `-j4` after each change. A failing test prints its assertion/subprocess output inline with `--output-on-failure`; without that flag, CTest only shows pass/fail per test name.
3535

3636
Each C++ test binary can also be run directly (bypassing CTest) for faster iteration:
3737

include/MeshCraft/GraphicsBackendCheck.hpp

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,20 +6,17 @@
66
// messages) so it is headlessly unit-testable without a full alternate-
77
// backend rebuild -- see test/graphics_backend_check_test.cpp.
88
//
9-
// The editor's ImGui UI is hard-wired to OpenGL/GLES3 (see
10-
// src/MeshCraft/MeshCraftApplication.cpp's ImGui_ImplOpenGL3_Init /
11-
// ImGui_ImplSDL3_InitForOpenGL) and only actually renders under EASYGL.
9+
// The editor's ImGui renderer uses CNA rather than a native graphics API.
1210

1311
#include <string>
1412

1513
namespace MeshCraft {
1614

17-
// Returns true if `backend` should be allowed to proceed (EASYGL always is;
18-
// any other backend only if `allowOverride` is set, e.g. from
19-
// MESH_CRAFT_ALLOW_UNSUPPORTED_BACKEND).
20-
inline bool isBackendSupportedAlg(const std::string& backend, bool allowOverride) {
21-
if (backend == "EASYGL") return true;
22-
return allowOverride;
15+
// The renderer has no native-GL dependency, but a backend becomes launchable
16+
// only after SYS-W8-05's real screenshot qualification. EASYGL is qualified
17+
// now; the other CNA backends stay truthfully rejected until then.
18+
inline bool isBackendSupportedAlg(const std::string& backend, bool /*allowOverride*/) {
19+
return backend == "EASYGL";
2320
}
2421

2522
} // namespace MeshCraft
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
#pragma once
2+
3+
#include <algorithm>
4+
#include <cmath>
5+
#include <optional>
6+
#include <vector>
7+
8+
namespace MeshCraft {
9+
10+
struct ImGuiScissorRect {
11+
int x;
12+
int y;
13+
int width;
14+
int height;
15+
};
16+
17+
// Converts Dear ImGui's display-relative clip rectangle to a bounded CNA
18+
// scissor rectangle. Kept graphics-context-free for regression tests.
19+
inline std::optional<ImGuiScissorRect> makeImGuiScissorRect(
20+
float left, float top, float right, float bottom,
21+
float displayX, float displayY, float scaleX, float scaleY,
22+
float framebufferWidth, float framebufferHeight)
23+
{
24+
const int x = std::max(0, static_cast<int>(std::floor((left - displayX) * scaleX)));
25+
const int y = std::max(0, static_cast<int>(std::floor((top - displayY) * scaleY)));
26+
const int r = std::min(static_cast<int>(framebufferWidth),
27+
static_cast<int>(std::ceil((right - displayX) * scaleX)));
28+
const int b = std::min(static_cast<int>(framebufferHeight),
29+
static_cast<int>(std::ceil((bottom - displayY) * scaleY)));
30+
if (r <= x || b <= y) return std::nullopt;
31+
return ImGuiScissorRect{x, y, r - x, b - y};
32+
}
33+
34+
// CNA user-indexed draws have no base-vertex parameter. Copy the command's
35+
// indices into a short-lived local range relative to its vertex offset.
36+
template <typename Index>
37+
inline bool localizeImGuiIndices(const Index* source, unsigned int sourceCount,
38+
unsigned int indexOffset, unsigned int elementCount,
39+
unsigned int vertexOffset, std::vector<Index>& destination)
40+
{
41+
if (indexOffset > sourceCount || elementCount > sourceCount - indexOffset)
42+
return false;
43+
destination.resize(elementCount);
44+
for (unsigned int i = 0; i < elementCount; ++i) {
45+
const Index index = source[indexOffset + i];
46+
if (index < vertexOffset) return false;
47+
destination[i] = static_cast<Index>(index - vertexOffset);
48+
}
49+
return true;
50+
}
51+
52+
} // namespace MeshCraft
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
#pragma once
2+
3+
#include <cstdint>
4+
#include <memory>
5+
6+
struct ImDrawData;
7+
struct SDL_Window;
8+
9+
namespace Microsoft::Xna::Framework::Graphics {
10+
class GraphicsDevice;
11+
class Texture2D;
12+
}
13+
14+
namespace MeshCraft {
15+
16+
// MeshCraft's rendering half of Dear ImGui. SDL remains the platform/input
17+
// backend; this class deliberately renders ImDrawData only through CNA.
18+
class ImGuiRenderer {
19+
public:
20+
virtual ~ImGuiRenderer() = default;
21+
virtual bool initialize(Microsoft::Xna::Framework::Graphics::GraphicsDevice& device,
22+
SDL_Window* window) = 0;
23+
virtual void newFrame() = 0;
24+
virtual void render(ImDrawData* drawData) = 0;
25+
virtual void shutdown() = 0;
26+
27+
// ImTextureID is opaque. MeshCraft stores this integer token rather than
28+
// leaking a native API handle into panels that call ImGui::Image().
29+
virtual std::uintptr_t registerTexture(Microsoft::Xna::Framework::Graphics::Texture2D& texture) = 0;
30+
virtual void unregisterTexture(std::uintptr_t token) = 0;
31+
32+
static std::unique_ptr<ImGuiRenderer> createCnaRenderer();
33+
};
34+
35+
} // namespace MeshCraft
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
#pragma once
2+
3+
#include <cstdint>
4+
#include <unordered_map>
5+
6+
namespace Microsoft::Xna::Framework::Graphics { class Texture2D; }
7+
8+
namespace MeshCraft {
9+
10+
// Backend-neutral ownership-free mapping between Dear ImGui's opaque texture
11+
// token and the CNA texture that the renderer will bind for a draw command.
12+
class ImGuiTextureRegistry {
13+
public:
14+
std::uintptr_t registerTexture(Microsoft::Xna::Framework::Graphics::Texture2D* texture) {
15+
if (!texture) return 0;
16+
const std::uintptr_t token = nextToken_++;
17+
textures_.emplace(token, texture);
18+
return token;
19+
}
20+
21+
Microsoft::Xna::Framework::Graphics::Texture2D* resolve(std::uintptr_t token) const {
22+
const auto found = textures_.find(token);
23+
return found == textures_.end() ? nullptr : found->second;
24+
}
25+
26+
void unregisterTexture(std::uintptr_t token) { textures_.erase(token); }
27+
void clear() { textures_.clear(); }
28+
29+
private:
30+
std::unordered_map<std::uintptr_t, Microsoft::Xna::Framework::Graphics::Texture2D*> textures_;
31+
std::uintptr_t nextToken_{1};
32+
};
33+
34+
} // namespace MeshCraft

0 commit comments

Comments
 (0)