载入中...
搜索中...
未找到
ImGuiBackend.cpp
浏览该文件的文档.
2#include "ui/Theme.h"
3
4#include "common/Exception.h"
6#include "common/config.h"
7#include "graphics/Graphics.h"
8
9#include <imgui.h>
10#include <imgui_internal.h>
11#include <imgui_impl_sdl.h>
12
13#ifdef EVENGINE_WEBGPU
15#include "imgui_impl_wgpu.h"
16#else
18#include <imgui_impl_vulkan.h>
19#include <vulkan/vulkan.h>
20#include <SDL2/SDL_vulkan.h>
21#endif
22
23#include <algorithm>
24#include <cmath>
25#include <cstdio>
26#include <cstdlib>
27#include <filesystem>
28#include <string>
29#include <vector>
30
31
32namespace eve::ui {
33namespace {
34
35#ifndef EVENGINE_WEBGPU
36void checkVk(VkResult err) {
37 if (err == 0) return;
38 throw eve::Exception("ImGui Vulkan error: VkResult = %d", int(err));
39}
40#endif
41
43constexpr float kBaseFontSizePx = 17.f;
44
46constexpr ImWchar kCjkProbeCodepoint = 0x4E2D;
48constexpr ImWchar kIconProbeCodepoint = 0xF002;
49
50std::vector<const char *> regularFontCandidates() {
51#if defined(_WIN32)
52 static const std::string winDir = [] {
53 const char *dir = getenv("WINDIR");
54 return dir ? std::string(dir) : std::string("C:\\Windows");
55 }();
56 static const std::vector<std::string> paths = {
57 winDir + "\\Fonts\\segoeui.ttf", winDir + "\\Fonts\\arial.ttf",
58 winDir + "\\Fonts\\tahoma.ttf", winDir + "\\Fonts\\msyh.ttc",
59 };
60#elif defined(__APPLE__)
61 static const std::vector<std::string> paths = {
62 "/System/Library/Fonts/Helvetica.ttc",
63 "/System/Library/Fonts/SFNS.ttf",
64 "/Library/Fonts/Arial.ttf",
65 };
66#elif defined(EVENGINE_ANDROID)
67 static const std::vector<std::string> paths = {
68 "/system/fonts/Roboto-Regular.ttf",
69 "/system/fonts/NotoSansCJK-Regular.ttc",
70 "/system/fonts/DroidSansFallback.ttf",
71 };
72#else
73 static const std::vector<std::string> paths = {
74 "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
75 "/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf",
76 "/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc",
77 "/usr/share/fonts/truetype/wqy/wqy-microhei.ttc",
78 "/usr/share/fonts/truetype/arphic/uming.ttc",
79 };
80#endif
81 std::vector<const char *> out;
82 out.reserve(paths.size());
83 for (const auto &p : paths) out.push_back(p.c_str());
84 return out;
85}
86
87// System fonts that actually contain CJK glyphs, tried in order when the
88// regular candidates above (Segoe UI / DejaVu Sans / Helvetica, ...) turn out
89// to be Latin-only. Each platform's first entry is the most common CJK font.
90std::vector<const char *> cjkFontCandidates() {
91#if defined(_WIN32)
92 static const std::string winDir = [] {
93 const char *dir = getenv("WINDIR");
94 return dir ? std::string(dir) : std::string("C:\\Windows");
95 }();
96 static const std::vector<std::string> paths = {
97 winDir + "\\Fonts\\msyh.ttc", // Microsoft YaHei (simplified + traditional)
98 winDir + "\\Fonts\\simhei.ttf", // SimHei
99 winDir + "\\Fonts\\simsun.ttc", // SimSun
100 winDir + "\\Fonts\\Deng.ttf", // DengXian
101 winDir + "\\Fonts\\msjh.ttc", // Microsoft JhengHei (traditional)
102 };
103#elif defined(__APPLE__)
104 static const std::vector<std::string> paths = {
105 "/System/Library/Fonts/PingFang.ttc",
106 "/System/Library/Fonts/STHeiti Light.ttc",
107 "/System/Library/Fonts/Hiragino Sans GB.ttc",
108 "/System/Library/Fonts/Supplemental/Songti.ttc",
109 "/Library/Fonts/Arial Unicode.ttf",
110 };
111#elif defined(EVENGINE_ANDROID)
112 static const std::vector<std::string> paths = {
113 "/system/fonts/NotoSansCJK-Regular.ttc",
114 "/system/fonts/DroidSansFallback.ttf",
115 "/system/fonts/NotoSansSC-Regular.otf",
116 };
117#else
118 static const std::vector<std::string> paths = {
119 "/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc",
120 "/usr/share/fonts/noto-cjk/NotoSansCJK-Regular.ttc",
121 "/usr/share/fonts/opentype/noto/NotoSansCJKsc-Regular.otf",
122 "/usr/share/fonts/truetype/wqy/wqy-microhei.ttc",
123 "/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc",
124 "/usr/share/fonts/truetype/droid/DroidSansFallbackFull.ttf",
125 "/usr/share/fonts/truetype/arphic/uming.ttc",
126 };
127#endif
128 std::vector<const char *> out;
129 out.reserve(paths.size());
130 for (const auto &p : paths) out.push_back(p.c_str());
131 return out;
132}
133
134std::vector<std::string> iconFontCandidates() {
135 std::vector<std::string> paths;
136 if (const char *overridePath = getenv("EVENGINE_ICON_FONT")) {
137 if (*overridePath) paths.emplace_back(overridePath);
138 }
139 if (char *basePath = SDL_GetBasePath()) {
140 const std::filesystem::path base(basePath);
141 // Build-tree layout: <build>/src/engine/eve.exe -> <build>/share/eve/fonts.
142 paths.push_back(
143 (base / "../../share/eve/fonts/FontAwesome.ttf").lexically_normal().string());
144 // Installed SDK layouts used by the packaged executable.
145 paths.push_back(
146 (base / "../../../share/eve/fonts/FontAwesome.ttf").lexically_normal().string());
147 paths.push_back(
148 (base / "../share/eve/fonts/FontAwesome.ttf").lexically_normal().string());
149 SDL_free(basePath);
150 }
151 paths.emplace_back("share/eve/fonts/FontAwesome.ttf");
152 paths.emplace_back("fonts/FontAwesome.ttf");
153 paths.emplace_back("test/fonts/FontAwesome.ttf");
154 return paths;
155}
156
157bool fileExists(const char *path) {
158 if (!path || !*path) return false;
159 FILE *f = fopen(path, "rb");
160 if (!f) return false;
161 fclose(f);
162 return true;
163}
164
165} // namespace
166
167std::unique_ptr<UIBackend> createImGuiBackend() {
168 return std::make_unique<ImGuiBackend>();
169}
170
172
173bool ImGuiBackend::init(SDL_Window *window, eve::graphics::Graphics *gfx) {
174 if (initialized_) return true;
175 if (!window || !gfx) return false;
176 StartupStage initStage("ui: ImGui backend init (first frame)");
177
178 gfx_ = gfx;
179 window_ = window;
180 dpiScale_ = computeDpiScale();
181 uiScale_ = computeInitialScale();
182
183 IMGUI_CHECKVERSION();
184 ctx_ = ::ImGui::CreateContext();
185 // Start from unified theme tokens (not a one-off ImGui palette).
186 setThemeDpiScale(dpiScale_);
187 setThemeUiScale(uiScale_);
188 applyThemeToImGui(globalTheme(), uiScale_);
189
190 ImGui_ImplSDL2_InitForVulkan(window);
191
192#ifdef EVENGINE_WEBGPU
193 auto *wgg = dynamic_cast<eve::graphics::webgpu::Graphics *>(gfx);
194 if (!wgg) return false;
195 ImGui_ImplWGPU_Init(wgg->getDevice().Get(), 2, wgg->getSurfaceFormat());
196 loadFonts();
198 fontsUploaded_ = true;
199 checkFontCoverage();
200#else
201 auto *vkg = dynamic_cast<eve::graphics::vulkan::Graphics *>(gfx);
202 if (!vkg) return false;
203 if (!vkg->getSwapchainRenderPass()) return false;
204
205 auto &device = vkg->getDevice();
206 VkDescriptorPoolSize poolSizes[] = {
207 {VK_DESCRIPTOR_TYPE_SAMPLER, 1000},
208 {VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1000},
209 {VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, 1000},
210 {VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, 1000},
211 {VK_DESCRIPTOR_TYPE_UNIFORM_TEXEL_BUFFER, 1000},
212 {VK_DESCRIPTOR_TYPE_STORAGE_TEXEL_BUFFER, 1000},
213 {VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, 1000},
214 {VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1000},
215 {VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC, 1000},
216 {VK_DESCRIPTOR_TYPE_STORAGE_BUFFER_DYNAMIC, 1000},
217 {VK_DESCRIPTOR_TYPE_INPUT_ATTACHMENT, 1000},
218 };
219 VkDescriptorPoolCreateInfo poolInfo{};
220 poolInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO;
221 poolInfo.flags = VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT;
222 poolInfo.maxSets = 1000 * uint32_t(sizeof(poolSizes) / sizeof(poolSizes[0]));
223 poolInfo.poolSizeCount = uint32_t(sizeof(poolSizes) / sizeof(poolSizes[0]));
224 poolInfo.pPoolSizes = poolSizes;
225
226 VkDescriptorPool pool = VK_NULL_HANDLE;
227 checkVk(vkCreateDescriptorPool(static_cast<VkDevice>(device.instance), &poolInfo, nullptr, &pool));
228 imguiDescriptorPool_ = pool;
229
230 uint32_t imageCount = vkg->getSwapchainImageCount();
231 if (imageCount < 2) imageCount = 2;
232
233 vkg->ensureUiColorResources();
234
235 ImGui_ImplVulkan_InitInfo initInfo{};
236 initInfo.Instance = static_cast<VkInstance>(vkg->getInstance().instance);
237 initInfo.PhysicalDevice = static_cast<VkPhysicalDevice>(device.physical_device.instance);
238 initInfo.Device = static_cast<VkDevice>(device.instance);
239 initInfo.QueueFamily = device.get_queue_index(vkb::QueueType::graphics);
240 initInfo.Queue = static_cast<VkQueue>(device.getQueue(vkb::QueueType::graphics));
241 initInfo.PipelineCache = VK_NULL_HANDLE;
242 initInfo.DescriptorPool = pool;
243 initInfo.MinImageCount = imageCount;
244 initInfo.ImageCount = imageCount;
245 initInfo.MSAASamples = static_cast<VkSampleCountFlagBits>(vkg->getUiMsaaSamples());
246 initInfo.CheckVkResultFn = [](VkResult err) {
247 if (err != 0) fprintf(stderr, "ImGui Vulkan: VkResult %d\n", int(err));
248 };
249
250 ImGui_ImplVulkan_Init(&initInfo, static_cast<VkRenderPass>(vkg->getUiMsaaRenderPass()));
251
252 {
253 StartupStage fontStage(" ui: font load + atlas upload");
254 loadFonts();
255
256 vkb::executeImmediately(device.instance, vkg->getUploadPool(),
257 device.getQueue(vkb::QueueType::graphics), [&](vk::CommandBuffer cb) {
258 ImGui_ImplVulkan_CreateFontsTexture(static_cast<VkCommandBuffer>(cb));
259 });
260 ImGui_ImplVulkan_DestroyFontUploadObjects();
261 fontsUploaded_ = true;
262 checkFontCoverage();
263 }
264#endif
265
266 initialized_ = true;
267 gfx_->addWindowDestroyedCallback(&ImGuiBackend::windowDestroyedThunk, this);
268 return true;
269}
270
271void ImGuiBackend::windowDestroyedThunk(void *userdata) {
272 auto *self = static_cast<ImGuiBackend *>(userdata);
273 if (self) self->shutdown();
274}
275
277 if (!initialized_) return;
278 if (frameOpen_) {
279 if (GImGui && GImGui->WithinFrameScope) ImGui::EndFrame();
280 frameOpen_ = false;
281 }
282 if (gfx_) {
283 if (gfx_->getPresentOverlayUser() == this) gfx_->setPresentOverlay(nullptr, nullptr);
284 }
285#ifdef EVENGINE_WEBGPU
287 ImGui_ImplSDL2_Shutdown();
288 ImGui::DestroyContext();
289#else
290 auto *vkg = dynamic_cast<eve::graphics::vulkan::Graphics *>(gfx_);
291 if (vkg) {
292 vkDeviceWaitIdle(static_cast<VkDevice>(vkg->getDevice().instance));
293 }
294 // The ImGui backend data lives on the context this backend created. It may
295 // not be the currently active context (headless tests switch contexts), so
296 // explicitly select it before tearing down ImGui_Impl* state.
297 if (ctx_) {
298 ::ImGuiContext *prev = ::ImGui::GetCurrentContext();
299 ::ImGuiContext *mine = ctx_;
300 ::ImGui::SetCurrentContext(mine);
301 ::ImGui_ImplVulkan_Shutdown();
302 ::ImGui_ImplSDL2_Shutdown();
303 ::ImGui::DestroyContext(mine);
304 ctx_ = nullptr;
305 // Restore the previous current context unless it was the one we just
306 // destroyed (DestroyContext already reset it to null).
307 if (prev && prev != mine) ::ImGui::SetCurrentContext(prev);
308 else ::ImGui::SetCurrentContext(nullptr);
309 }
310 if (imguiDescriptorPool_ && vkg) {
311 vkDestroyDescriptorPool(static_cast<VkDevice>(vkg->getDevice().instance),
312 static_cast<VkDescriptorPool>(imguiDescriptorPool_), nullptr);
313 imguiDescriptorPool_ = nullptr;
314 }
315 if (imguiTexturePool_ && vkg) {
316 vkDestroyDescriptorPool(static_cast<VkDevice>(vkg->getDevice().instance),
317 static_cast<VkDescriptorPool>(imguiTexturePool_), nullptr);
318 imguiTexturePool_ = nullptr;
319 }
320 if (imguiTextureLayout_ && vkg) {
321 vkDestroyDescriptorSetLayout(static_cast<VkDevice>(vkg->getDevice().instance),
322 static_cast<VkDescriptorSetLayout>(imguiTextureLayout_),
323 nullptr);
324 imguiTextureLayout_ = nullptr;
325 }
326#endif
327 textures_.clear();
328 queuedTextureDraws_.clear();
329 gfx_ = nullptr;
330 window_ = nullptr;
331 fontsUploaded_ = false;
332 initialized_ = false;
333}
334
335void ImGuiBackend::processEvent(const SDL_Event *event) {
336 if (!initialized_ || !event) return;
337 ImGui_ImplSDL2_ProcessEvent(event);
338}
339
341 if (!initialized_ || !window_) return;
342 queuedTextureDraws_.clear();
343 if (frameOpen_) {
344 if (GImGui && GImGui->WithinFrameScope) ImGui::EndFrame();
345 frameOpen_ = false;
346 }
347#ifdef EVENGINE_WEBGPU
349#else
350 ImGui_ImplVulkan_NewFrame();
351#endif
352 ImGui_ImplSDL2_NewFrame(window_);
353 ImGui::NewFrame();
354 frameOpen_ = true;
355 if (gfx_) gfx_->setPresentOverlay(&ImGuiBackend::presentOverlayThunk, this);
356}
357
358void ImGuiBackend::applyScale(float scale) {
359 if (!initialized_) return;
360 scale = std::clamp(scale, 0.5f, 5.f);
361 uiScale_ = scale;
362 setThemeDpiScale(dpiScale_);
365 // Atlas pixels are baked at dpiScale_, not uiScale_. Rebuilding here
366 // without Fonts->Clear() used to duplicate merged CJK fonts (tofu / missing
367 // glyphs) and cost another ~500ms. Logical size is FontGlobalScale only.
368}
369
371 if (!initialized_) return;
372 applyScale(scale);
373}
374
375float ImGuiBackend::computeInitialScale() const {
376#if defined(EVENGINE_ANDROID) || defined(EVENGINE_IOS)
377 float ddpi = 160.f;
378 if (SDL_GetDisplayDPI(0, &ddpi, nullptr, nullptr) != 0 || ddpi < 1.f) ddpi = 320.f;
379 float s = ddpi / 160.f;
380 return std::clamp(s, 1.75f, 3.25f);
381#elif defined(_WIN32)
382 // A DPI-aware SDL 2.0 window uses physical pixels for both its window and
383 // drawable size, so their ratio stays 1 even at 125%/150% Windows scale.
384 // Scale ImGui geometry explicitly to retain the OS-requested UI size.
385 return computeDpiScale();
386#else
387 // Desktop: ImGui's backend already applies the display density via
388 // io.DisplayFramebufferScale, so the logical (point-space) UI scale stays
389 // 1.0. Keeping it constant makes the UI match the OS UI size on
390 // high-resolution displays instead of growing with the pixel ratio.
391 (void)window_;
392 return 1.f;
393#endif
394}
395
396float ImGuiBackend::computeDpiScale() const {
397#if defined(_WIN32)
398 const int display = window_ ? SDL_GetWindowDisplayIndex(window_) : 0;
399 float ddpi = 96.f;
400 if (display >= 0 && SDL_GetDisplayDPI(display, &ddpi, nullptr, nullptr) == 0 && ddpi > 0.f)
401 return std::clamp(ddpi / 96.f, 1.f, 4.f);
402 return 1.f;
403#else
404 int logicalW = 0, logicalH = 0, pixelW = 0, pixelH = 0;
405 SDL_GetWindowSize(window_, &logicalW, &logicalH);
406#ifdef EVENGINE_WEBGPU
407 // No GL context on WebGPU; drawable size == window size for the canvas.
408 SDL_GetWindowSize(window_, &pixelW, &pixelH);
409#else
410 SDL_Vulkan_GetDrawableSize(window_, &pixelW, &pixelH);
411#endif
412 if (logicalW > 0 && pixelW > 0) {
413 float s = float(pixelW) / float(logicalW);
414 if (s > 0.f) return std::clamp(s, 1.f, 4.f);
415 }
416 return 1.f;
417#endif
418}
419
420void ImGuiBackend::loadFonts() {
421 ImGuiIO &io = ImGui::GetIO();
422 ImFontAtlas *atlas = io.Fonts;
423 if (!atlas) return;
424
425 atlas->Clear();
426
427 // Rasterize at the physical DPI resolution so glyphs stay crisp; the
428 // FontGlobalScale set in applyThemeToImGui cancels this so the logical
429 // text size stays constant regardless of display density.
430 const float sizePx = kBaseFontSizePx * dpiScale_;
431
432 ImFontConfig cfg{};
433 cfg.OversampleH = 3;
434 cfg.OversampleV = 1;
435 cfg.PixelSnapH = true;
436 // A small coverage boost gives Segoe UI's thin strokes enough contrast
437 // after the physical-DPI atlas is scaled back into logical coordinates.
438 cfg.RasterizerMultiply = 1.12f;
439 // Request CJK ranges so the merged CJK font below rasterizes Chinese,
440 // Japanese and Korean text (the atlas grows by a few MB — acceptable).
441 fontRanges_.clear();
442 ImFontGlyphRangesBuilder rangeBuilder;
443 rangeBuilder.AddRanges(atlas->GetGlyphRangesDefault());
444 rangeBuilder.AddRanges(atlas->GetGlyphRangesChineseFull());
445 rangeBuilder.BuildRanges(&fontRanges_);
446 cfg.GlyphRanges = fontRanges_.Data;
447
448 bool added = false;
449 std::string primaryPath;
450 for (const char *path : regularFontCandidates()) {
451 if (!fileExists(path)) continue;
452 if (atlas->AddFontFromFileTTF(path, sizePx, &cfg)) {
453 primaryPath = path;
454 added = true;
455 break;
456 }
457 }
458 if (!added) atlas->AddFontDefault(&cfg);
459
460 // The first available regular font is usually Latin-only (Segoe UI,
461 // DejaVu Sans, Helvetica), so requesting CJK ranges on it adds no Chinese
462 // glyphs to the atlas. Merge a CJK-capable system font into the same atlas
463 // (same MergeMode pattern as the icon font below) so Chinese/Japanese text
464 // renders instead of ImGui's '?' fallback glyph.
465 //
466 // Glyph coverage cannot be verified until the atlas is rasterized (both
467 // ImFont::Glyphs and the IndexLookup table are populated during Build), so
468 // pick by file identity: every cjkFontCandidates() entry is CJK-capable,
469 // and the first existing one is the best match for the platform. Whether
470 // the merge actually covered CJK is checked after upload in
471 // checkFontCoverage().
472 cjkRanges_.clear();
473 ImFontGlyphRangesBuilder cjkRangeBuilder;
474 cjkRangeBuilder.AddRanges(atlas->GetGlyphRangesChineseFull());
475 cjkRangeBuilder.BuildRanges(&cjkRanges_);
476 bool primaryIsCjk = false;
477 for (const char *path : cjkFontCandidates()) {
478 if (primaryPath == path) {
479 primaryIsCjk = true;
480 break;
481 }
482 }
483 if (!primaryIsCjk) {
484 ImFontConfig cjkCfg{};
485 // CJK glyphs are dense (ChineseFull is ~20k+ codepoints); the physical
486 // size already scales with DPI, so oversampling 1 keeps the atlas sane.
487 cjkCfg.OversampleH = 1;
488 cjkCfg.OversampleV = 1;
489 cjkCfg.MergeMode = true;
490 cjkCfg.GlyphRanges = cjkRanges_.Data;
491 for (const char *path : cjkFontCandidates()) {
492 if (!fileExists(path)) continue;
493 if (atlas->AddFontFromFileTTF(path, sizePx, &cjkCfg, cjkRanges_.Data)) break;
494 }
495 }
496
497 ImFontConfig iconCfg{};
498 iconCfg.OversampleH = 2;
499 iconCfg.OversampleV = 2;
500 iconCfg.MergeMode = true;
501 iconCfg.PixelSnapH = true;
502 static const ImWchar iconRanges[] = {0xF000, 0xF8FF, 0};
503 for (const std::string &path : iconFontCandidates()) {
504 if (!fileExists(path.c_str())) continue;
505 if (atlas->AddFontFromFileTTF(path.c_str(), sizePx, &iconCfg, iconRanges)) break;
506 }
507}
508
509void ImGuiBackend::checkFontCoverage() const {
510 // Only valid after the atlas has been built/uploaded (lookup tables exist).
511 ImFontAtlas *atlas = ImGui::GetIO().Fonts;
512 if (!atlas || atlas->Fonts.Size == 0) return;
513 const ImFont *font = atlas->Fonts[0];
514 if (font && font->FindGlyphNoFallback(kCjkProbeCodepoint) == nullptr)
515 fprintf(stderr,
516 "[ui] warning: no CJK-capable system font found; Chinese text will render as '?'\n");
517 if (font && font->FindGlyphNoFallback(kIconProbeCodepoint) == nullptr)
518 fprintf(stderr,
519 "[ui] warning: editor icon font not found; semantic icons will render as '?'\n");
520}
521
522void ImGuiBackend::rebuildFonts() {
523#ifdef EVENGINE_WEBGPU
524 loadFonts();
527 fontsUploaded_ = true;
528#else
529 auto *vkg = dynamic_cast<eve::graphics::vulkan::Graphics *>(gfx_);
530 if (!vkg) return;
531 loadFonts();
532 ImGui_ImplVulkan_DestroyFontUploadObjects();
533 auto &device = vkg->getDevice();
534 vkb::executeImmediately(device.instance, vkg->getUploadPool(),
535 device.getQueue(vkb::QueueType::graphics),
536 [&](vk::CommandBuffer cb) {
537 ImGui_ImplVulkan_CreateFontsTexture(
538 static_cast<VkCommandBuffer>(cb));
539 });
540 ImGui_ImplVulkan_DestroyFontUploadObjects();
541 fontsUploaded_ = true;
542#endif
543}
544
545uint64_t ImGuiBackend::registerTexture(graphics::Texture *tex) {
546 if (!initialized_ || !tex || !tex->gpuHandle) return 0;
547 RegisteredTexture reg;
548 reg.texture = tex;
549#ifdef EVENGINE_WEBGPU
550 auto *gt = static_cast<eve::graphics::webgpu::GpuTexture *>(tex->gpuHandle);
551 reg.imId = (ImTextureID)(intptr_t)gt->view.Get();
552 if (!reg.imId) return 0;
553#else
554 auto *vkg = dynamic_cast<eve::graphics::vulkan::Graphics *>(gfx_);
555 if (!vkg) return 0;
556 auto *gt = static_cast<eve::graphics::vulkan::GpuTexture *>(tex->gpuHandle);
557 const VkDevice device = static_cast<VkDevice>(vkg->getDevice().instance);
558 if (!imguiTextureLayout_) {
559 VkDescriptorSetLayoutBinding binding{};
560 binding.binding = 0;
561 binding.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
562 binding.descriptorCount = 1;
563 binding.stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT;
564 VkDescriptorSetLayoutCreateInfo info{};
565 info.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO;
566 info.bindingCount = 1;
567 info.pBindings = &binding;
568 VkDescriptorSetLayout layout = VK_NULL_HANDLE;
569 checkVk(vkCreateDescriptorSetLayout(device, &info, nullptr, &layout));
570 imguiTextureLayout_ = layout;
571 }
572 if (!imguiTexturePool_) {
573 VkDescriptorPoolSize size{};
574 size.type = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
575 size.descriptorCount = 512;
576 VkDescriptorPoolCreateInfo info{};
577 info.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO;
578 info.flags = VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT;
579 info.maxSets = 512;
580 info.poolSizeCount = 1;
581 info.pPoolSizes = &size;
582 VkDescriptorPool pool = VK_NULL_HANDLE;
583 checkVk(vkCreateDescriptorPool(device, &info, nullptr, &pool));
584 imguiTexturePool_ = pool;
585 }
586 VkDescriptorSet set = VK_NULL_HANDLE;
587 VkDescriptorSetAllocateInfo ai{};
588 ai.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO;
589 ai.descriptorPool = static_cast<VkDescriptorPool>(imguiTexturePool_);
590 ai.descriptorSetCount = 1;
591 const VkDescriptorSetLayout layout = static_cast<VkDescriptorSetLayout>(imguiTextureLayout_);
592 ai.pSetLayouts = &layout;
593 checkVk(vkAllocateDescriptorSets(device, &ai, &set));
594 VkDescriptorImageInfo imageInfo{};
595 imageInfo.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
596 imageInfo.imageView = static_cast<VkImageView>(gt->imageView());
597 imageInfo.sampler = static_cast<VkSampler>(gt->sampler);
598 VkWriteDescriptorSet write{};
599 write.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
600 write.dstSet = set;
601 write.dstBinding = 0;
602 write.descriptorCount = 1;
603 write.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
604 write.pImageInfo = &imageInfo;
605 vkUpdateDescriptorSets(device, 1, &write, 0, nullptr);
606 reg.imId = set;
607#endif
608 reg.width = tex->getWidth();
609 reg.height = tex->getHeight();
610 const uint64_t key = nextTextureKey_++;
611 textures_[key] = reg;
612 return key;
613}
614
615void ImGuiBackend::unregisterTexture(uint64_t id) {
616 auto it = textures_.find(id);
617 if (it == textures_.end()) return;
618#ifndef EVENGINE_WEBGPU
619 auto *vkg = dynamic_cast<eve::graphics::vulkan::Graphics *>(gfx_);
620 if (vkg && imguiTexturePool_) {
621 VkDescriptorSet set = static_cast<VkDescriptorSet>(it->second.imId);
622 vkFreeDescriptorSets(static_cast<VkDevice>(vkg->getDevice().instance),
623 static_cast<VkDescriptorPool>(imguiTexturePool_), 1, &set);
624 }
625#endif
626 textures_.erase(it);
627}
628
629bool ImGuiBackend::textureSize(uint64_t id, int *w, int *h) const {
630 auto it = textures_.find(id);
631 if (it == textures_.end()) return false;
632 if (w) *w = it->second.width;
633 if (h) *h = it->second.height;
634 return true;
635}
636
637void *ImGuiBackend::textureHandle(uint64_t id) const {
638 auto it = textures_.find(id);
639 return it == textures_.end() ? nullptr : static_cast<void *>(it->second.imId);
640}
641
642bool ImGuiBackend::usesQueuedTextureDraws() const {
643#ifdef EVENGINE_WEBGPU
644 return false;
645#else
646 // The pinned ImGui 1.83 Vulkan renderer always binds its font descriptor
647 // and ignores ImDrawCmd::TextureId. Draw-list callbacks preserve the position
648 // of each registered texture relative to text, child windows and popups.
649 return true;
650#endif
651}
652
653void ImGuiBackend::queueTextureDraw(uint64_t id, float x, float y, float w, float h, float u0,
654 float v0, float u1, float v1, float r, float g, float b,
655 float a, bool opaque) {
656 if (!usesQueuedTextureDraws() || id == 0 || textures_.find(id) == textures_.end()) return;
657 const ImVec2 scale = ImGui::GetIO().DisplayFramebufferScale;
658 const ImVec4 clip = ImGui::GetWindowDrawList()->_ClipRectStack.back();
659 queuedTextureDraws_.push_back({id,
660 x * scale.x,
661 y * scale.y,
662 w * scale.x,
663 h * scale.y,
664 u0,
665 v0,
666 u1,
667 v1,
668 r,
669 g,
670 b,
671 a,
672 clip.x * scale.x,
673 clip.y * scale.y,
674 (clip.z - clip.x) * scale.x,
675 (clip.w - clip.y) * scale.y,
676 opaque,
677 this,
678 queuedTextureDraws_.size()});
679 ImDrawList* list = ImGui::GetWindowDrawList();
680 list->AddCallback(textureDrawThunk, &queuedTextureDraws_.back());
681 list->AddCallback(ImDrawCallback_ResetRenderState, nullptr);
682}
683
685 if (!initialized_) return false;
686 return ImGui::GetIO().WantCaptureMouse;
687}
688
690 if (!initialized_) return false;
691 return ImGui::GetIO().WantCaptureKeyboard;
692}
693
694void ImGuiBackend::renderDrawData(void *commandBuffer) {
695 if (!initialized_ || !commandBuffer) return;
696 if (!GImGui || !GImGui->WithinFrameScope) return;
697 ImGui::Render();
698 frameOpen_ = false;
699#ifdef EVENGINE_WEBGPU
700 WGPURenderPassEncoder enc = *static_cast<WGPURenderPassEncoder *>(commandBuffer);
701 ImGui_ImplWGPU_RenderDrawData(ImGui::GetDrawData(), enc);
702#else
703 activeCommandBuffer_ = commandBuffer;
704 try {
705 ImGui_ImplVulkan_RenderDrawData(ImGui::GetDrawData(), static_cast<VkCommandBuffer>(commandBuffer));
706 } catch (...) {
707 activeCommandBuffer_ = nullptr;
708 throw;
709 }
710 activeCommandBuffer_ = nullptr;
711#endif
712}
713
714void ImGuiBackend::textureDrawThunk(const ImDrawList*, const ImDrawCmd* command) {
715#ifndef EVENGINE_WEBGPU
716 const auto& queued = *static_cast<const QueuedTextureDraw*>(command->UserCallbackData);
717 auto& owner = *queued.owner;
718 auto* vkg = dynamic_cast<eve::graphics::vulkan::Graphics*>(owner.gfx_);
719 if (!vkg || !owner.activeCommandBuffer_) return;
720 const auto found = owner.textures_.find(queued.id);
721 if (found == owner.textures_.end() || !found->second.texture) return;
723 draw.texture = found->second.texture;
724 draw.x = queued.x;
725 draw.y = queued.y;
726 draw.w = queued.w;
727 draw.h = queued.h;
728 draw.u0 = queued.u0;
729 draw.v0 = queued.v0;
730 draw.u1 = queued.u1;
731 draw.v1 = queued.v1;
733 draw.clipX = queued.clipX;
734 draw.clipY = queued.clipY;
735 draw.clipW = queued.clipW;
736 draw.clipH = queued.clipH;
737 draw.opaque = queued.opaque;
738 vkg->drawUiTextureRects(owner.activeCommandBuffer_, {draw}, queued.bufferIndex);
739#else
740 (void)command;
741#endif
742}
743
744bool ImGuiBackend::presentOverlayThunk(void *userdata, void *commandBuffer) {
745 auto *self = static_cast<ImGuiBackend *>(userdata);
746 if (!self || !self->initialized_ || !GImGui || !GImGui->WithinFrameScope) return false;
747 if (commandBuffer) self->renderDrawData(commandBuffer);
748 return true;
749}
750
751} // namespace eve::ui
float w
Definition AnimClip.cpp:738
float y
Definition AnimClip.cpp:738
float x
Definition AnimClip.cpp:738
const std::string & s
glm::vec4 p[6]
std::string layout
tensor::Graph g
Definition GpuGraph.cpp:7
vkb::Device & device
std::uint32_t key
glm::vec4 clip
double r
int h
std::array< float, 3 > scale
MeleePoint3 b
Definition MeleeHit.cpp:41
MeleePoint3 a
Definition MeleeHit.cpp:40
MeshVfxBatchedDraw draw
size_t queued
Definition OnnxGpgpu.cpp:60
eve::action::ActionVfxBinding binding
float f
std::string path
Definition PlayHost.cpp:110
std::string id
Definition PlayHost.cpp:108
bool found
std::string string
float size
Definition TreeMesh.cpp:156
V3 dir
Definition TreeMesh.cpp:150
std::map< std::string, std::string > paths
EVENGINE_API_FOUNDATION public API.
Definition Exception.h:13
StartupStage public API.
void addWindowDestroyedCallback(WindowDestroyedCallback cb, void *userdata)
Adds window destroyed callback.
Definition Graphics.h:1596
void * getPresentOverlayUser() const
Returns the present overlay user.
Definition Graphics.h:1615
void setPresentOverlay(PresentOverlayFn fn, void *userdata)
Sets the present overlay.
Definition Graphics.h:1608
EVENGINE_API_BACKENDS public API.
Definition Graphics.h:342
vkb::Device & getDevice()
Returns the device.
Definition Graphics.h:1014
EVENGINE_API_BACKENDS public API.
Definition Graphics.h:191
Dear ImGui + SDL input + Vulkan present overlay.
void shutdown() override
Shutdown.
bool wantCaptureKeyboard() const override
Want capture keyboard.
void newFrame() override
Creates a frame. @ownership Caller deletes unless documented otherwise.
bool init(SDL_Window *window, eve::graphics::Graphics *gfx) override
Initializes .
~ImGuiBackend() override
Releases ImGuiBackend resources.
ImGuiBackend()=default
Constructs a ImGuiBackend.
bool wantCaptureMouse() const override
Want capture mouse.
void setScale(float scale) override
Sets the scale.
void processEvent(const SDL_Event *event) override
Process event.
IMGUI_IMPL_API void ImGui_ImplWGPU_NewFrame()
Im gui impl wgpu new frame.
IMGUI_IMPL_API bool ImGui_ImplWGPU_CreateFontsTexture()
Im gui impl wgpu create fonts texture.
IMGUI_IMPL_API void ImGui_ImplWGPU_RenderDrawData(ImDrawData *draw_data, WGPURenderPassEncoder pass_encoder)
Im gui impl wgpu render draw data.
IMGUI_IMPL_API bool ImGui_ImplWGPU_Init(WGPUDevice device, int num_frames_in_flight, WGPUTextureFormat rt_format)
Im gui impl wgpu init.
IMGUI_IMPL_API void ImGui_ImplWGPU_Shutdown()
Im gui impl wgpu shutdown.
IMGUI_IMPL_API void ImGui_ImplWGPU_InvalidateDeviceObjects()
Im gui impl wgpu invalidate device objects.
eve::Color Color
RGBA color used by every graphics draw call. Lives inside eve::graphics so including a graphics heade...
Definition Color.h:13
void setThemeDpiScale(float dpiScale)
Sets the theme dpi scale.
Definition Theme.cpp:307
void setThemeUiScale(float scale)
Logical (point-space) UI scale. Default 1.0.
Definition Theme.cpp:301
WidgetDesc window(std::string title, std::vector< WidgetDesc > children, std::string id)
Top-level window widget with a title bar.
Definition Widget.cpp:292
Theme & globalTheme()
Global theme.
Definition Theme.cpp:277
void applyThemeToImGui(const Theme &theme)
Push tokens into ImGui style. Metrics are multiplied by uiScale (default: themeUiScale()).
Definition Theme.cpp:313
std::unique_ptr< UIBackend > createImGuiBackend()
Default backend: Dear ImGui + SDL + Vulkan (see ui/imgui/).
WidgetDesc list(std::string listId, const std::vector< std::string > &items, const std::function< WidgetDesc(const std::string &, int)> &itemFn)
Expand a string list into a Group of item widgets. itemFn(label, index) builds each row; keys default...
Definition Widget.cpp:715
GpuTexture public API.
Definition Graphics.h:257
Backend-internal textured rectangle queued by a UI presenter.
Definition Graphics.h:122
Texture resources backed by a wgpu texture + view + sampler + bind groups.
Definition Graphics.h:131
glm::uvec4 info