From 6fd3ed434d829ffb19dc8d8a0bdddf518b490bf7 Mon Sep 17 00:00:00 2001 From: ssdfasd <2156608475@qq.com> Date: Sun, 5 Apr 2026 15:16:15 +0800 Subject: [PATCH] De-ImGui XCUI standalone text atlas provider --- docs/plan/XCUI_Phase_Status_2026-04-05.md | 43 +- .../XCUIStandaloneTextAtlasProvider.cpp | 828 +++++++++++++++--- .../XCUIStandaloneTextAtlasProvider.h | 12 +- tests/NewEditor/CMakeLists.txt | 15 +- ...st_xcui_standalone_text_atlas_provider.cpp | 114 ++- 5 files changed, 879 insertions(+), 133 deletions(-) diff --git a/docs/plan/XCUI_Phase_Status_2026-04-05.md b/docs/plan/XCUI_Phase_Status_2026-04-05.md index 9b19db55..37622565 100644 --- a/docs/plan/XCUI_Phase_Status_2026-04-05.md +++ b/docs/plan/XCUI_Phase_Status_2026-04-05.md @@ -16,7 +16,7 @@ Old `editor` replacement is explicitly out of scope for this phase. - `LayoutLab` continues as the editor widget proving ground for tree/list/property-section style controls - the demo sandbox and editor bridge APIs were tightened again without touching the old editor replacement scope - `new_editor` now has an explicit two-step window compositor seam through `IWindowUICompositor` / `ImGuiWindowUICompositor`, layered on top of `IEditorHostCompositor` / `ImGuiHostCompositor` -- The native-host follow-up is now landed in `new_editor`: +- The native-host / hosted-preview publication follow-up is now landed in `new_editor`: - `NativeWindowUICompositor` is now buildable alongside the legacy ImGui compositor - `Application` now defaults to a native XCUI host path instead of creating the ImGui shell by default - the native default path now drives `XCUIDemoRuntime` and `XCUILayoutLabRuntime` directly, composes their `UIDrawData`, and submits one swapchain packet through the native compositor @@ -24,6 +24,9 @@ Old `editor` replacement is explicitly out of scope for this phase. - the native shell now begins the hosted-preview queue/registry lifecycle each frame, queues native preview frames, drains them during the native render pass, and consumes published hosted-surface images directly in panel cards with live/warming placeholder states - the old ImGui shell path remains present as an explicit compatibility host instead of the default host - `XCUIInputBridge.h` no longer drags `imgui.h` through the public XCUI input seam +- The default native text path is now also de-ImGuiized inside `new_editor`: + - `XCUIStandaloneTextAtlasProvider` now builds and owns its atlas through a Windows/GDI raster path instead of using ImGui font-atlas internals + - the standalone atlas provider now exposes both `RGBA32` and `Alpha8` views, supports non-nominal size resolution, lazily adds non-prebaked BMP glyphs, and falls back to `?` when a glyph cannot be rasterized - Old `editor` replacement remains deferred; all active execution still stays inside XCUI shared code and `new_editor`. ## Three-Layer Status @@ -50,7 +53,7 @@ Current gap: - Minimal schema self-definition support is landed, including consistency checks for enum/document-only schema metadata, but schema-driven validation for `.xcui` / `.xctheme` instances is still not implemented. - Shared widget/runtime instantiation is still thin and mostly editor-side. -- Common widget primitives are still incomplete: shared text-input presentation/composition on top of the new text controller, multi-selection/focus-traversal/virtualized collection state on top of the new editor-primitive helpers, and a fully native text/font-atlas path that no longer leans on ImGui font internals. +- Common widget primitives are still incomplete: shared text-input presentation/composition on top of the new text controller, multi-selection/focus-traversal/virtualized collection state on top of the new editor-primitive helpers, shared scroll/navigation-scope/caret-layout helpers, and promotion of the current native text-atlas path into a shared/cross-platform text subsystem. ### 2. Runtime/Game Layer @@ -118,9 +121,10 @@ Current gap: Current gap: - The default shell host is now native, but the legacy ImGui shell and panel path still exists as a compatibility host and is still compiled into `new_editor`. +- The default native shell path is still compiled through an `Application` translation unit that directly includes legacy ImGui host/presenter/compositor code, and `new_editor/CMakeLists.txt` still treats those legacy ImGui sources and include paths as target-global defaults. - The native shell currently proves direct runtime composition, but its shell chrome is still a bespoke `Application`-side layout rather than a fully shared XCUI-authored editor shell document. - Editor-specialized widgets are still incomplete at the shared-module level: the authored prototypes exist, but virtualization, multi-selection/focus traversal, toolbar/menu chrome, menu interaction widgets, and icon-atlas widgets are not yet extracted into reusable XCUI modules. -- The default native path still depends on ImGui font-atlas internals through `XCUIStandaloneTextAtlasProvider`, so text rendering is not yet cleanly de-ImGuiized even though the shell/panel composition path is native by default. +- The default native text path now uses a standalone Windows/GDI atlas through `XCUIStandaloneTextAtlasProvider`, but that provider still lives inside `new_editor` and is not yet promoted into a shared/cross-platform text subsystem. ## Validated This Phase @@ -131,17 +135,19 @@ Current gap: - `new_editor_xcui_layout_lab_runtime_tests`: `12/12` - `new_editor_xcui_rhi_command_compiler_tests`: `7/7` - `new_editor_xcui_rhi_render_backend_tests`: `5/5` +- `new_editor_xcui_standalone_text_atlas_provider_tests`: `6/6` - `new_editor_xcui_hosted_preview_presenter_tests`: `20/20` - `new_editor_imgui_window_ui_compositor_tests`: `7/7` - `new_editor_native_window_ui_compositor_tests`: `8/8` - `new_editor_xcui_editor_command_router_tests`: `5/5` -- `new_editor_application_shell_command_bindings_tests`: `8/8` +- `new_editor_application_shell_command_bindings_tests`: `11/11` - `new_editor_xcui_shell_chrome_state_tests`: `11/11` - `new_editor_xcui_panel_canvas_host_tests`: `4/4` - `new_editor_imgui_xcui_panel_canvas_host_tests`: `1/1` - `new_editor_native_xcui_panel_canvas_host_tests`: `4/4` - `new_editor_xcui_layout_lab_panel_tests`: `3/3` - `XCNewEditor` Debug target builds successfully +- `XCNewEditor.exe` native-default smoke run stayed alive for `5s` - `core_ui_tests`: `52 total` (`50` passed, `2` skipped because `KeyCode::Delete` currently aliases `Backspace`) - `scene_tests`: `68/68` - `core_ui_style_tests`: `5/5` @@ -253,7 +259,7 @@ Current gap: - generic preview frame submission no longer carries an ImGui draw-list pointer - the ImGui presenter now resolves inline draw targets through an explicit ImGui-only binding seam - panel/runtime callers still preserve the same legacy and native-preview behavior -- Native hosted-preview publication is now wired through the default shell path: +- Native hosted-preview publication milestone is now wired through the default shell path: - `NativeWindowUICompositor` publishes hosted preview textures as SRV-backed XCUI registrations and frees them through the compositor seam - `Application::BeginHostedPreviewFrameLifecycle(...)` now resets queue/registry state for both legacy and native frame paths - hosted-preview surface readiness now keys on published texture availability instead of ImGui-style descriptor validity @@ -266,19 +272,38 @@ Current gap: - XCUI asset document loading changed to prefer direct source compilation before `ResourceManager` fallback for the sandbox path, fixing the LayoutLab crash. - `UIDocumentCompiler.cpp` repaired enough to restore full local builds after the duplicated schema-helper regression. - MSVC debug build hardening was tightened again so large parallel `engine` rebuilds stop tripping over compile-PDB contention. +- `XCUIStandaloneTextAtlasProvider` no longer uses ImGui font-atlas/internal baking helpers: + - atlas ownership now stays inside a standalone provider implementation built on Windows/GDI glyph rasterization + - default editor atlas prewarms the current supported nominal sizes, exposes both `RGBA32` and `Alpha8` atlas views, lazily inserts non-prebaked BMP glyphs, and falls back to `?` for unrasterizable codepoints + - standalone atlas coverage now includes reset/rebuild, non-nominal size resolution, lazy glyph insertion/fallback behavior, and smoke use without any ImGui context ## Phase Risks Still Open - Schema instance validation is still open beyond `.xcschema` self-definition and artifact round-trip coverage. - `ScrollView` is still authored/static; no wheel-driven scrolling or virtualization yet. -- The default native text path still builds its font atlas on top of ImGui font types/internal baking helpers. +- The default native shell path still compiles through an `Application.cpp` unit that directly pulls in ImGui compatibility host code, so the native default path is not yet isolated at the translation-unit boundary. +- `new_editor/CMakeLists.txt` still builds the legacy ImGui host/compositor/backend sources and include directories as target-global defaults instead of a compatibility-only slice. +- The default native text path now owns its atlas without ImGui, but the provider is still Windows-only and remains trapped inside `new_editor` instead of a shared/cross-platform text layer. - Hosted-preview compatibility presentation still depends on an ImGui-only inline presenter path when not using the queued native surface path. - Editor widget coverage is still prototype-driven inside `LayoutLab`; it has not yet been promoted into a full reusable shared widget/runtime layer with command routing, virtualization, and property-edit transactions. +## Execution-Plan Alignment + +- Against `XCUI完整架构设计与执行计划.md`, current `new_editor` progress should be treated as an early `Phase 8` foothold rather than full `Milestone E` completion: + - landed: `NativeWindowUICompositor`, native shell packet composition, native hosted-preview publication, XCUI-owned texture registrations, native panel surface-image presentation, standalone native text-atlas ownership inside `new_editor` + - not yet landed: default-path translation-unit isolation from legacy ImGui host code, promotion of the native text-atlas path into a shared/cross-platform text subsystem, shared XCUI-authored editor shell chrome +- That means the next de-ImGui push should not keep centering on hosted-preview publication; that milestone is now effectively closed for the default native shell path. +- The real remaining default-path blockers are: + - isolate legacy ImGui shell/compositor/presenter wiring from the default `Application` build path + - stop treating ImGui include paths and backend sources as the default `new_editor` compilation surface + - harden and promote `XCUIStandaloneTextAtlasProvider` / editor font bootstrap into a shared native text subsystem + - move native shell chrome out of bespoke `Application` layout code and into a shared XCUI shell model or authored shell document + ## Next Phase 1. Expand runtime/game-layer ownership from the current `SceneRuntime` UI context into scene-declared HUD/menu bootstrapping, draw submission, and higher-level runtime UI policies. 2. Promote the current editor-facing widget prototypes out of authored `LayoutLab` content and into reusable XCUI widget/runtime modules, then continue with toolbar/menu chrome, shell-state adoption, virtualization, and broader focus/multi-selection behavior. -3. Strip the remaining default-path ImGui seams down to compatibility-only code, starting with the standalone text/font-atlas path and then the remaining hosted-preview fallback layers. -4. Promote the native shell chrome and card layout out of bespoke `Application` code into a shared XCUI/editor-layer shell model or authored shell document. -5. Continue phased validation, commit, push, and plan refresh after each stable batch. +3. Isolate legacy ImGui shell/compositor/presenter wiring from the default native path first, starting with `Application` translation-unit separation and a `new_editor` build split that stops treating ImGui include paths and sources as the default compilation surface. +4. Promote the current standalone native text/font path out of `new_editor`, harden its atlas invalidation/caching contract, and remove the remaining default-path ImGui text/bootstrap ownership around compatibility-only code. +5. Promote the native shell chrome and card layout out of bespoke `Application` code into a shared XCUI/editor-layer shell model or authored shell document. +6. Continue phased validation, commit, push, and plan refresh after each stable batch. diff --git a/new_editor/src/XCUIBackend/XCUIStandaloneTextAtlasProvider.cpp b/new_editor/src/XCUIBackend/XCUIStandaloneTextAtlasProvider.cpp index 2394c298..9e551089 100644 --- a/new_editor/src/XCUIBackend/XCUIStandaloneTextAtlasProvider.cpp +++ b/new_editor/src/XCUIBackend/XCUIStandaloneTextAtlasProvider.cpp @@ -1,6 +1,21 @@ #include "XCUIBackend/XCUIStandaloneTextAtlasProvider.h" -#include +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include namespace XCEngine { namespace Editor { @@ -8,65 +23,688 @@ namespace XCUIBackend { namespace { -IXCUITextAtlasProvider::FontHandle MakeFontHandle(const ImFont* font) { +constexpr float kDefaultNominalFontSize = 18.0f; +constexpr int kAtlasPadding = 1; +constexpr int kMaxAtlasDimension = 4096; +constexpr std::size_t kInvalidFontIndex = (std::numeric_limits::max)(); +constexpr wchar_t kPrimaryFontFace[] = L"Segoe UI"; +constexpr wchar_t kFallbackFontFace[] = L"Microsoft YaHei"; +constexpr std::array kSupportedFontSizes = { 13, 14, 16, 18, 20 }; +constexpr std::array, 2> kPrebakedCodepointRanges = {{ + { 0x0020u, 0x007Eu }, + { 0x00A0u, 0x00FFu }, +}}; + +struct CachedPixelBuffer { + std::vector bytes = {}; + int width = 0; + int height = 0; + int bytesPerPixel = 0; + + void Clear() { + bytes.clear(); + width = 0; + height = 0; + bytesPerPixel = 0; + } + + bool IsValid() const { + return !bytes.empty() && width > 0 && height > 0 && bytesPerPixel > 0; + } +}; + +struct GlyphRecord { + int sizeKey = 0; + std::uint32_t codepoint = 0u; + bool visible = false; + bool colored = false; + float advanceX = 0.0f; + float x0 = 0.0f; + float y0 = 0.0f; + float x1 = 0.0f; + float y1 = 0.0f; + int bitmapWidth = 0; + int bitmapHeight = 0; + int atlasX = 0; + int atlasY = 0; + std::vector alpha = {}; +}; + +struct BakedFontRecord { + int sizeKey = 0; + IXCUITextAtlasProvider::BakedFontInfo metrics = {}; + std::unordered_map glyphs = {}; +}; + +IXCUITextAtlasProvider::FontHandle MakeDefaultFontHandle() { IXCUITextAtlasProvider::FontHandle handle = {}; - handle.value = reinterpret_cast(font); + handle.value = 1u; return handle; } -ImFont* ResolveFontHandle(IXCUITextAtlasProvider::FontHandle handle) { - return reinterpret_cast(handle.value); +bool IsDefaultFontHandle(IXCUITextAtlasProvider::FontHandle handle) { + return handle.value == MakeDefaultFontHandle().value; } -float ResolveNominalFontSize(const ImFont& font) { - return font.LegacySize > 0.0f ? font.LegacySize : 16.0f; +std::size_t ResolveFontIndex(IXCUITextAtlasProvider::FontHandle handle) { + return IsDefaultFontHandle(handle) ? 0u : kInvalidFontIndex; } -float ResolveRequestedFontSize(const ImFont& font, float requestedFontSize) { - return requestedFontSize > 0.0f ? requestedFontSize : ResolveNominalFontSize(font); +std::uint64_t MakeGlyphKey(int sizeKey, std::uint32_t codepoint) { + return (static_cast(static_cast(sizeKey)) << 32u) | + static_cast(codepoint); } -bool IsFontOwnedByAtlas(const ImFont* font, const ImFontAtlas* atlas) { - return font != nullptr && atlas != nullptr && font->OwnerAtlas == atlas; +void AppendCodepointRange( + std::vector& outCodepoints, + std::uint32_t first, + std::uint32_t last) { + if (first > last) { + return; + } + + outCodepoints.reserve(outCodepoints.size() + static_cast(last - first + 1u)); + for (std::uint32_t codepoint = first; codepoint <= last; ++codepoint) { + outCodepoints.push_back(codepoint); + } } -ImFontBaked* ResolveBakedFont( - IXCUITextAtlasProvider::FontHandle fontHandle, - ImFontAtlas* atlas, - float requestedFontSize) { - ImFont* font = ResolveFontHandle(fontHandle); - if (!IsFontOwnedByAtlas(font, atlas)) { +std::vector BuildPrebakedCodepointSet() { + std::vector codepoints = {}; + codepoints.reserve(768u); + for (const auto& range : kPrebakedCodepointRanges) { + AppendCodepointRange(codepoints, range.first, range.second); + } + codepoints.push_back(0xFFFDu); + std::sort(codepoints.begin(), codepoints.end()); + codepoints.erase(std::unique(codepoints.begin(), codepoints.end()), codepoints.end()); + return codepoints; +} + +HFONT CreateEditorFont(int pixelHeight, const wchar_t* faceName) { + return ::CreateFontW( + -pixelHeight, + 0, + 0, + 0, + FW_NORMAL, + FALSE, + FALSE, + FALSE, + DEFAULT_CHARSET, + OUT_TT_PRECIS, + CLIP_DEFAULT_PRECIS, + ANTIALIASED_QUALITY, + DEFAULT_PITCH | FF_DONTCARE, + faceName); +} + +bool QueryTextMetrics(HDC dc, HFONT font, TEXTMETRICW& outMetrics) { + outMetrics = {}; + if (dc == nullptr || font == nullptr) { + return false; + } + + HGDIOBJ previous = ::SelectObject(dc, font); + const BOOL result = ::GetTextMetricsW(dc, &outMetrics); + ::SelectObject(dc, previous); + return result != FALSE; +} + +bool FontHasGlyph(HDC dc, HFONT font, std::uint32_t codepoint) { + if (dc == nullptr || font == nullptr || codepoint > 0xFFFFu) { + return false; + } + + WORD glyphIndex = 0xFFFFu; + WCHAR wideCodepoint = static_cast(codepoint); + HGDIOBJ previous = ::SelectObject(dc, font); + const DWORD result = ::GetGlyphIndicesW( + dc, + &wideCodepoint, + 1u, + &glyphIndex, + GGI_MARK_NONEXISTING_GLYPHS); + ::SelectObject(dc, previous); + return result != GDI_ERROR && glyphIndex != 0xFFFFu; +} + +bool IsAdvanceOnlyWhitespace(std::uint32_t codepoint) { + return codepoint == 0x0020u || + codepoint == 0x00A0u || + (codepoint >= 0x2000u && codepoint <= 0x200Au) || + codepoint == 0x202Fu || + codepoint == 0x205Fu || + codepoint == 0x3000u; +} + +TEXTMETRICW MergeTextMetrics( + const TEXTMETRICW& primaryMetrics, + bool hasPrimaryMetrics, + const TEXTMETRICW& fallbackMetrics, + bool hasFallbackMetrics) { + TEXTMETRICW merged = {}; + if (hasPrimaryMetrics) { + merged = primaryMetrics; + } + if (!hasFallbackMetrics) { + return merged; + } + if (!hasPrimaryMetrics) { + return fallbackMetrics; + } + + merged.tmHeight = (std::max)(primaryMetrics.tmHeight, fallbackMetrics.tmHeight); + merged.tmAscent = (std::max)(primaryMetrics.tmAscent, fallbackMetrics.tmAscent); + merged.tmDescent = (std::max)(primaryMetrics.tmDescent, fallbackMetrics.tmDescent); + merged.tmExternalLeading = (std::max)(primaryMetrics.tmExternalLeading, fallbackMetrics.tmExternalLeading); + merged.tmAveCharWidth = (std::max)(primaryMetrics.tmAveCharWidth, fallbackMetrics.tmAveCharWidth); + merged.tmMaxCharWidth = (std::max)(primaryMetrics.tmMaxCharWidth, fallbackMetrics.tmMaxCharWidth); + return merged; +} + +class GdiFontContext { +public: + explicit GdiFontContext(int sizeKey) + : m_sizeKey(sizeKey) { + m_dc = ::CreateCompatibleDC(nullptr); + if (m_dc == nullptr) { + return; + } + + m_primaryFont = CreateEditorFont(sizeKey, kPrimaryFontFace); + m_fallbackFont = CreateEditorFont(sizeKey, kFallbackFontFace); + m_hasPrimaryMetrics = QueryTextMetrics(m_dc, m_primaryFont, m_primaryMetrics); + m_hasFallbackMetrics = QueryTextMetrics(m_dc, m_fallbackFont, m_fallbackMetrics); + m_lineMetrics = MergeTextMetrics( + m_primaryMetrics, + m_hasPrimaryMetrics, + m_fallbackMetrics, + m_hasFallbackMetrics); + } + + ~GdiFontContext() { + if (m_primaryFont != nullptr) { + ::DeleteObject(m_primaryFont); + m_primaryFont = nullptr; + } + if (m_fallbackFont != nullptr) { + ::DeleteObject(m_fallbackFont); + m_fallbackFont = nullptr; + } + if (m_dc != nullptr) { + ::DeleteDC(m_dc); + m_dc = nullptr; + } + } + + bool IsValid() const { + return m_dc != nullptr && (m_hasPrimaryMetrics || m_hasFallbackMetrics); + } + + IXCUITextAtlasProvider::BakedFontInfo BuildBakedFontInfo() const { + IXCUITextAtlasProvider::BakedFontInfo info = {}; + info.lineHeight = static_cast(m_lineMetrics.tmHeight); + info.ascent = static_cast(m_lineMetrics.tmAscent); + info.descent = static_cast(m_lineMetrics.tmDescent); + info.rasterizerDensity = 1.0f; + return info; + } + + bool RasterizeGlyph(std::uint32_t codepoint, GlyphRecord& outRecord) const { + outRecord = {}; + outRecord.sizeKey = m_sizeKey; + outRecord.codepoint = codepoint; + outRecord.colored = false; + + if (!IsValid()) { + return false; + } + + HFONT resolvedFont = ResolveFontForCodepoint(codepoint); + if (resolvedFont == nullptr) { + return false; + } + + if (IsAdvanceOnlyWhitespace(codepoint)) { + HGDIOBJ previous = ::SelectObject(m_dc, resolvedFont); + SIZE textExtent = {}; + WCHAR wideCodepoint = static_cast(codepoint); + const BOOL extentResult = ::GetTextExtentPoint32W(m_dc, &wideCodepoint, 1, &textExtent); + ::SelectObject(m_dc, previous); + outRecord.visible = false; + outRecord.advanceX = extentResult != FALSE + ? static_cast(textExtent.cx) + : static_cast(m_lineMetrics.tmAveCharWidth); + return true; + } + + HGDIOBJ previous = ::SelectObject(m_dc, resolvedFont); + MAT2 transform = {}; + transform.eM11.value = 1; + transform.eM22.value = 1; + + GLYPHMETRICS glyphMetrics = {}; + const DWORD bufferSize = ::GetGlyphOutlineW( + m_dc, + static_cast(codepoint), + GGO_GRAY8_BITMAP, + &glyphMetrics, + 0u, + nullptr, + &transform); + ::SelectObject(m_dc, previous); + + if (bufferSize == GDI_ERROR) { + return false; + } + + outRecord.visible = glyphMetrics.gmBlackBoxX > 0u && glyphMetrics.gmBlackBoxY > 0u; + outRecord.advanceX = static_cast(glyphMetrics.gmCellIncX); + outRecord.bitmapWidth = static_cast(glyphMetrics.gmBlackBoxX); + outRecord.bitmapHeight = static_cast(glyphMetrics.gmBlackBoxY); + outRecord.x0 = static_cast(glyphMetrics.gmptGlyphOrigin.x); + outRecord.y0 = static_cast(m_lineMetrics.tmAscent - glyphMetrics.gmptGlyphOrigin.y); + outRecord.x1 = outRecord.x0 + static_cast(glyphMetrics.gmBlackBoxX); + outRecord.y1 = outRecord.y0 + static_cast(glyphMetrics.gmBlackBoxY); + + if (!outRecord.visible) { + return true; + } + + std::vector bitmapBuffer(bufferSize); + previous = ::SelectObject(m_dc, resolvedFont); + const DWORD writeResult = ::GetGlyphOutlineW( + m_dc, + static_cast(codepoint), + GGO_GRAY8_BITMAP, + &glyphMetrics, + bufferSize, + bitmapBuffer.data(), + &transform); + ::SelectObject(m_dc, previous); + if (writeResult == GDI_ERROR) { + return false; + } + + const int sourceStride = (static_cast(glyphMetrics.gmBlackBoxX) + 3) & ~3; + const std::size_t requiredBytes = + static_cast(sourceStride) * + static_cast((std::max)(0, outRecord.bitmapHeight)); + if (bitmapBuffer.size() < requiredBytes) { + return false; + } + outRecord.alpha.resize( + static_cast(outRecord.bitmapWidth) * + static_cast(outRecord.bitmapHeight)); + for (int y = 0; y < outRecord.bitmapHeight; ++y) { + const unsigned char* sourceRow = bitmapBuffer.data() + static_cast(y * sourceStride); + unsigned char* destRow = outRecord.alpha.data() + + static_cast(y * outRecord.bitmapWidth); + for (int x = 0; x < outRecord.bitmapWidth; ++x) { + const unsigned int value = static_cast(sourceRow[x]); + destRow[x] = static_cast((value * 255u + 32u) / 64u); + } + } + + return true; + } + +private: + HFONT ResolveFontForCodepoint(std::uint32_t codepoint) const { + if (FontHasGlyph(m_dc, m_primaryFont, codepoint)) { + return m_primaryFont; + } + if (FontHasGlyph(m_dc, m_fallbackFont, codepoint)) { + return m_fallbackFont; + } return nullptr; } - const float resolvedFontSize = ResolveRequestedFontSize(*font, requestedFontSize); - if (resolvedFontSize <= 0.0f) { - return nullptr; + int m_sizeKey = 0; + HDC m_dc = nullptr; + HFONT m_primaryFont = nullptr; + HFONT m_fallbackFont = nullptr; + TEXTMETRICW m_primaryMetrics = {}; + TEXTMETRICW m_fallbackMetrics = {}; + TEXTMETRICW m_lineMetrics = {}; + bool m_hasPrimaryMetrics = false; + bool m_hasFallbackMetrics = false; +}; + +std::uintptr_t MakePixelDataKey(const CachedPixelBuffer& pixels, std::uint64_t generation) { + return reinterpret_cast(pixels.bytes.data()) ^ + static_cast(generation * 1315423911ull); +} + +int ResolveSupportedSizeKey(float requestedSize, float nominalSize) { + const float targetSize = requestedSize > 0.0f ? requestedSize : nominalSize; + auto bestIt = std::min_element( + kSupportedFontSizes.begin(), + kSupportedFontSizes.end(), + [targetSize](int lhs, int rhs) { + const float lhsDistance = std::fabs(targetSize - static_cast(lhs)); + const float rhsDistance = std::fabs(targetSize - static_cast(rhs)); + return lhsDistance < rhsDistance; + }); + return bestIt != kSupportedFontSizes.end() ? *bestIt : static_cast(std::lround(targetSize)); +} + +bool PopulateGlyphInfo( + const GlyphRecord& glyph, + int atlasWidth, + int atlasHeight, + IXCUITextAtlasProvider::GlyphInfo& outInfo) { + outInfo = {}; + outInfo.requestedCodepoint = glyph.codepoint; + outInfo.resolvedCodepoint = glyph.codepoint; + outInfo.visible = glyph.visible; + outInfo.colored = glyph.colored; + outInfo.advanceX = glyph.advanceX; + outInfo.x0 = glyph.x0; + outInfo.y0 = glyph.y0; + outInfo.x1 = glyph.x1; + outInfo.y1 = glyph.y1; + if (!glyph.visible) { + return true; + } + if (atlasWidth <= 0 || atlasHeight <= 0) { + return false; } - return font->GetFontBaked(resolvedFontSize); + outInfo.u0 = static_cast(glyph.atlasX) / static_cast(atlasWidth); + outInfo.v0 = static_cast(glyph.atlasY) / static_cast(atlasHeight); + outInfo.u1 = static_cast(glyph.atlasX + glyph.bitmapWidth) / static_cast(atlasWidth); + outInfo.v1 = static_cast(glyph.atlasY + glyph.bitmapHeight) / static_cast(atlasHeight); + return true; +} + +std::vector BuildVisiblePackOrder(const std::vector& glyphs) { + std::vector order = {}; + order.reserve(glyphs.size()); + for (std::size_t index = 0; index < glyphs.size(); ++index) { + if (glyphs[index].visible) { + order.push_back(index); + } + } + + std::sort( + order.begin(), + order.end(), + [&glyphs](std::size_t lhs, std::size_t rhs) { + const GlyphRecord& left = glyphs[lhs]; + const GlyphRecord& right = glyphs[rhs]; + if (left.bitmapHeight != right.bitmapHeight) { + return left.bitmapHeight > right.bitmapHeight; + } + if (left.bitmapWidth != right.bitmapWidth) { + return left.bitmapWidth > right.bitmapWidth; + } + if (left.sizeKey != right.sizeKey) { + return left.sizeKey < right.sizeKey; + } + return left.codepoint < right.codepoint; + }); + return order; +} + +bool TryPackVisibleGlyphs( + std::vector& glyphs, + int atlasWidth, + int& outAtlasHeight) { + outAtlasHeight = 1; + if (atlasWidth <= 0) { + return false; + } + + for (GlyphRecord& glyph : glyphs) { + glyph.atlasX = 0; + glyph.atlasY = 0; + } + + const std::vector packOrder = BuildVisiblePackOrder(glyphs); + int cursorX = kAtlasPadding; + int cursorY = kAtlasPadding; + int rowHeight = 0; + + for (std::size_t index : packOrder) { + GlyphRecord& glyph = glyphs[index]; + const int requiredWidth = glyph.bitmapWidth + kAtlasPadding * 2; + const int requiredHeight = glyph.bitmapHeight + kAtlasPadding * 2; + if (requiredWidth > atlasWidth) { + return false; + } + + if (cursorX + requiredWidth > atlasWidth) { + cursorX = kAtlasPadding; + cursorY += rowHeight; + rowHeight = 0; + } + + glyph.atlasX = cursorX + kAtlasPadding; + glyph.atlasY = cursorY + kAtlasPadding; + cursorX += requiredWidth; + rowHeight = (std::max)(rowHeight, requiredHeight); + outAtlasHeight = (std::max)(outAtlasHeight, cursorY + rowHeight + kAtlasPadding); + if (outAtlasHeight > kMaxAtlasDimension) { + return false; + } + } + + return true; } } // namespace -XCUIStandaloneTextAtlasProvider::XCUIStandaloneTextAtlasProvider() { +struct XCUIStandaloneTextAtlasProvider::Impl { + bool ready = false; + float nominalSize = kDefaultNominalFontSize; + CachedPixelBuffer alpha8Pixels = {}; + CachedPixelBuffer rgba32Pixels = {}; + std::uint64_t pixelGeneration = 0u; + std::unordered_map bakedFonts = {}; + std::vector glyphRecords = {}; + std::unordered_map glyphIndices = {}; +}; + +namespace { + +void ResetImpl(XCUIStandaloneTextAtlasProvider::Impl& impl) { + impl.ready = false; + impl.nominalSize = kDefaultNominalFontSize; + impl.alpha8Pixels.Clear(); + impl.rgba32Pixels.Clear(); + impl.bakedFonts.clear(); + impl.glyphRecords.clear(); + impl.glyphIndices.clear(); + ++impl.pixelGeneration; +} + +bool RebuildAtlasPixels(XCUIStandaloneTextAtlasProvider::Impl& impl) { + int chosenAtlasWidth = 0; + int chosenAtlasHeight = 1; + for (const int candidateWidth : { 1024, 2048, 4096 }) { + int packedHeight = 1; + if (TryPackVisibleGlyphs(impl.glyphRecords, candidateWidth, packedHeight)) { + chosenAtlasWidth = candidateWidth; + chosenAtlasHeight = packedHeight; + break; + } + } + + if (chosenAtlasWidth == 0 || chosenAtlasHeight <= 0) { + return false; + } + + impl.alpha8Pixels.width = chosenAtlasWidth; + impl.alpha8Pixels.height = chosenAtlasHeight; + impl.alpha8Pixels.bytesPerPixel = 1; + impl.alpha8Pixels.bytes.assign( + static_cast(chosenAtlasWidth) * static_cast(chosenAtlasHeight), + 0u); + + for (const GlyphRecord& glyph : impl.glyphRecords) { + if (!glyph.visible || glyph.bitmapWidth <= 0 || glyph.bitmapHeight <= 0) { + continue; + } + + for (int y = 0; y < glyph.bitmapHeight; ++y) { + const unsigned char* sourceRow = glyph.alpha.data() + + static_cast(y * glyph.bitmapWidth); + unsigned char* destRow = impl.alpha8Pixels.bytes.data() + + static_cast((glyph.atlasY + y) * impl.alpha8Pixels.width + glyph.atlasX); + std::copy(sourceRow, sourceRow + glyph.bitmapWidth, destRow); + } + } + + impl.rgba32Pixels.width = chosenAtlasWidth; + impl.rgba32Pixels.height = chosenAtlasHeight; + impl.rgba32Pixels.bytesPerPixel = 4; + impl.rgba32Pixels.bytes.assign( + static_cast(chosenAtlasWidth) * + static_cast(chosenAtlasHeight) * + 4u, + 0u); + for (std::size_t pixelIndex = 0; pixelIndex < impl.alpha8Pixels.bytes.size(); ++pixelIndex) { + const unsigned char alpha = impl.alpha8Pixels.bytes[pixelIndex]; + unsigned char* rgba = impl.rgba32Pixels.bytes.data() + pixelIndex * 4u; + rgba[0] = 255u; + rgba[1] = 255u; + rgba[2] = 255u; + rgba[3] = alpha; + } + + for (auto& [sizeKey, bakedFont] : impl.bakedFonts) { + (void)sizeKey; + bakedFont.glyphs.clear(); + } + + for (const GlyphRecord& glyph : impl.glyphRecords) { + IXCUITextAtlasProvider::GlyphInfo glyphInfo = {}; + if (!PopulateGlyphInfo( + glyph, + impl.alpha8Pixels.width, + impl.alpha8Pixels.height, + glyphInfo)) { + return false; + } + impl.bakedFonts[glyph.sizeKey].glyphs.insert_or_assign(glyph.codepoint, glyphInfo); + } + + ++impl.pixelGeneration; + return true; +} + +bool AddGlyphRecord( + XCUIStandaloneTextAtlasProvider::Impl& impl, + int sizeKey, + std::uint32_t codepoint) { + const std::uint64_t glyphKey = MakeGlyphKey(sizeKey, codepoint); + if (impl.glyphIndices.find(glyphKey) != impl.glyphIndices.end()) { + return true; + } + + GdiFontContext context(sizeKey); + if (!context.IsValid()) { + return false; + } + + if (impl.bakedFonts.find(sizeKey) == impl.bakedFonts.end()) { + BakedFontRecord bakedFont = {}; + bakedFont.sizeKey = sizeKey; + bakedFont.metrics = context.BuildBakedFontInfo(); + impl.bakedFonts.insert_or_assign(sizeKey, std::move(bakedFont)); + } + + GlyphRecord glyph = {}; + if (!context.RasterizeGlyph(codepoint, glyph)) { + return false; + } + + const std::size_t previousGlyphCount = impl.glyphRecords.size(); + impl.glyphRecords.push_back(std::move(glyph)); + impl.glyphIndices.insert_or_assign(glyphKey, previousGlyphCount); + if (!RebuildAtlasPixels(impl)) { + impl.glyphIndices.erase(glyphKey); + impl.glyphRecords.pop_back(); + RebuildAtlasPixels(impl); + return false; + } + + return true; +} + +const BakedFontRecord* FindBakedFont( + const XCUIStandaloneTextAtlasProvider::Impl& impl, + int sizeKey) { + const auto it = impl.bakedFonts.find(sizeKey); + return it != impl.bakedFonts.end() ? &it->second : nullptr; +} + +} // namespace + +XCUIStandaloneTextAtlasProvider::XCUIStandaloneTextAtlasProvider() + : m_impl(std::make_unique()) { RebuildDefaultEditorAtlas(); } +XCUIStandaloneTextAtlasProvider::~XCUIStandaloneTextAtlasProvider() = default; + void XCUIStandaloneTextAtlasProvider::Reset() { - m_atlas.Clear(); - m_defaultFont = nullptr; - m_ready = false; + if (m_impl == nullptr) { + m_impl = std::make_unique(); + return; + } + + ResetImpl(*m_impl); } bool XCUIStandaloneTextAtlasProvider::RebuildDefaultEditorAtlas() { Reset(); - m_ready = BuildDefaultXCUIEditorFontAtlas(m_atlas, m_defaultFont); - return m_ready; + + if (m_impl == nullptr) { + return false; + } + + const std::vector prebakedCodepoints = BuildPrebakedCodepointSet(); + for (const int sizeKey : kSupportedFontSizes) { + GdiFontContext context(sizeKey); + if (!context.IsValid()) { + ResetImpl(*m_impl); + return false; + } + + BakedFontRecord bakedFont = {}; + bakedFont.sizeKey = sizeKey; + bakedFont.metrics = context.BuildBakedFontInfo(); + m_impl->bakedFonts.insert_or_assign(sizeKey, std::move(bakedFont)); + + for (std::uint32_t codepoint : prebakedCodepoints) { + GlyphRecord glyph = {}; + if (!context.RasterizeGlyph(codepoint, glyph)) { + continue; + } + + const std::uint64_t glyphKey = MakeGlyphKey(sizeKey, codepoint); + m_impl->glyphIndices.insert_or_assign(glyphKey, m_impl->glyphRecords.size()); + m_impl->glyphRecords.push_back(std::move(glyph)); + } + } + + if (m_impl->bakedFonts.empty() || !RebuildAtlasPixels(*m_impl)) { + ResetImpl(*m_impl); + return false; + } + + m_impl->ready = true; + return true; } bool XCUIStandaloneTextAtlasProvider::IsReady() const { - return m_ready && ResolveDefaultFont() != nullptr; + return m_impl != nullptr && + m_impl->ready && + !m_impl->bakedFonts.empty() && + m_impl->rgba32Pixels.IsValid(); } bool XCUIStandaloneTextAtlasProvider::GetAtlasTextureView( @@ -74,74 +712,59 @@ bool XCUIStandaloneTextAtlasProvider::GetAtlasTextureView( AtlasTextureView& outView) const { outView = {}; - ImFontAtlas* atlas = ResolveAtlas(); - if (atlas == nullptr) { + if (!IsReady() || m_impl == nullptr) { return false; } - unsigned char* pixels = nullptr; - int width = 0; - int height = 0; - int bytesPerPixel = 0; - PixelFormat resolvedFormat = preferredFormat; - - switch (preferredFormat) { - case PixelFormat::Alpha8: - atlas->GetTexDataAsAlpha8(&pixels, &width, &height, &bytesPerPixel); + const CachedPixelBuffer* resolvedPixels = nullptr; + PixelFormat resolvedFormat = PixelFormat::Unknown; + if (preferredFormat == PixelFormat::Alpha8 && m_impl->alpha8Pixels.IsValid()) { + resolvedPixels = &m_impl->alpha8Pixels; resolvedFormat = PixelFormat::Alpha8; - break; - case PixelFormat::RGBA32: - case PixelFormat::Unknown: - default: - atlas->GetTexDataAsRGBA32(&pixels, &width, &height, &bytesPerPixel); + } else if (m_impl->rgba32Pixels.IsValid()) { + resolvedPixels = &m_impl->rgba32Pixels; resolvedFormat = PixelFormat::RGBA32; - break; + } else if (m_impl->alpha8Pixels.IsValid()) { + resolvedPixels = &m_impl->alpha8Pixels; + resolvedFormat = PixelFormat::Alpha8; } - if (pixels == nullptr || width <= 0 || height <= 0 || bytesPerPixel <= 0) { + if (resolvedPixels == nullptr || !resolvedPixels->IsValid()) { return false; } - outView.pixels = pixels; - outView.width = width; - outView.height = height; - outView.stride = width * bytesPerPixel; - outView.bytesPerPixel = bytesPerPixel; + outView.pixels = resolvedPixels->bytes.data(); + outView.width = resolvedPixels->width; + outView.height = resolvedPixels->height; + outView.stride = resolvedPixels->width * resolvedPixels->bytesPerPixel; + outView.bytesPerPixel = resolvedPixels->bytesPerPixel; outView.format = resolvedFormat; - outView.atlasStorageKey = reinterpret_cast(atlas); - outView.pixelDataKey = reinterpret_cast(pixels); + outView.atlasStorageKey = reinterpret_cast(m_impl.get()); + outView.pixelDataKey = MakePixelDataKey(*resolvedPixels, m_impl->pixelGeneration); return true; } std::size_t XCUIStandaloneTextAtlasProvider::GetFontCount() const { - const ImFontAtlas* atlas = ResolveAtlas(); - return atlas != nullptr ? static_cast(atlas->Fonts.Size) : 0u; + return IsReady() ? 1u : 0u; } IXCUITextAtlasProvider::FontHandle XCUIStandaloneTextAtlasProvider::GetFont(std::size_t index) const { - const ImFontAtlas* atlas = ResolveAtlas(); - if (atlas == nullptr || index >= static_cast(atlas->Fonts.Size)) { - return {}; - } - - return MakeFontHandle(atlas->Fonts[static_cast(index)]); + return IsReady() && index == 0u ? MakeDefaultFontHandle() : FontHandle{}; } IXCUITextAtlasProvider::FontHandle XCUIStandaloneTextAtlasProvider::GetDefaultFont() const { - return MakeFontHandle(ResolveDefaultFont()); + return IsReady() ? MakeDefaultFontHandle() : FontHandle{}; } bool XCUIStandaloneTextAtlasProvider::GetFontInfo(FontHandle fontHandle, FontInfo& outInfo) const { outInfo = {}; - ImFontAtlas* atlas = ResolveAtlas(); - ImFont* font = ResolveFontHandle(fontHandle); - if (!IsFontOwnedByAtlas(font, atlas)) { + if (!IsReady() || ResolveFontIndex(fontHandle) == kInvalidFontIndex || m_impl == nullptr) { return false; } outInfo.handle = fontHandle; - outInfo.nominalSize = ResolveNominalFontSize(*font); + outInfo.nominalSize = m_impl->nominalSize; return true; } @@ -151,15 +774,17 @@ bool XCUIStandaloneTextAtlasProvider::GetBakedFontInfo( BakedFontInfo& outInfo) const { outInfo = {}; - ImFontBaked* bakedFont = ResolveBakedFont(fontHandle, ResolveAtlas(), fontSize); + if (!IsReady() || ResolveFontIndex(fontHandle) == kInvalidFontIndex || m_impl == nullptr) { + return false; + } + + const int sizeKey = ResolveSupportedSizeKey(fontSize, m_impl->nominalSize); + const BakedFontRecord* bakedFont = FindBakedFont(*m_impl, sizeKey); if (bakedFont == nullptr) { return false; } - outInfo.lineHeight = bakedFont->Size; - outInfo.ascent = bakedFont->Ascent; - outInfo.descent = bakedFont->Descent; - outInfo.rasterizerDensity = bakedFont->RasterizerDensity; + outInfo = bakedFont->metrics; return true; } @@ -170,53 +795,48 @@ bool XCUIStandaloneTextAtlasProvider::FindGlyph( GlyphInfo& outInfo) const { outInfo = {}; - if (codepoint > IM_UNICODE_CODEPOINT_MAX) { + if (!IsReady() || ResolveFontIndex(fontHandle) == kInvalidFontIndex || m_impl == nullptr) { return false; } - ImFontBaked* bakedFont = ResolveBakedFont(fontHandle, ResolveAtlas(), fontSize); + const int sizeKey = ResolveSupportedSizeKey(fontSize, m_impl->nominalSize); + const BakedFontRecord* bakedFont = FindBakedFont(*m_impl, sizeKey); if (bakedFont == nullptr) { return false; } - ImFontGlyph* glyph = bakedFont->FindGlyph(static_cast(codepoint)); - if (glyph == nullptr) { + const auto glyphIt = bakedFont->glyphs.find(codepoint); + if (glyphIt != bakedFont->glyphs.end()) { + outInfo = glyphIt->second; + return true; + } + + if (codepoint <= 0xFFFFu && AddGlyphRecord(*m_impl, sizeKey, codepoint)) { + const BakedFontRecord* refreshedBakedFont = FindBakedFont(*m_impl, sizeKey); + if (refreshedBakedFont != nullptr) { + const auto refreshedGlyphIt = refreshedBakedFont->glyphs.find(codepoint); + if (refreshedGlyphIt != refreshedBakedFont->glyphs.end()) { + outInfo = refreshedGlyphIt->second; + return true; + } + } + } + + const BakedFontRecord* fallbackBakedFont = FindBakedFont(*m_impl, sizeKey); + if (fallbackBakedFont == nullptr) { return false; } + const auto fallbackGlyphIt = fallbackBakedFont->glyphs.find(static_cast('?')); + if (fallbackGlyphIt == fallbackBakedFont->glyphs.end()) { + return false; + } + + outInfo = fallbackGlyphIt->second; outInfo.requestedCodepoint = codepoint; - outInfo.resolvedCodepoint = glyph->Codepoint; - outInfo.visible = glyph->Visible != 0; - outInfo.colored = glyph->Colored != 0; - outInfo.advanceX = glyph->AdvanceX; - outInfo.x0 = glyph->X0; - outInfo.y0 = glyph->Y0; - outInfo.x1 = glyph->X1; - outInfo.y1 = glyph->Y1; - outInfo.u0 = glyph->U0; - outInfo.v0 = glyph->V0; - outInfo.u1 = glyph->U1; - outInfo.v1 = glyph->V1; return true; } -::ImFontAtlas* XCUIStandaloneTextAtlasProvider::ResolveAtlas() const { - return m_ready ? &m_atlas : nullptr; -} - -::ImFont* XCUIStandaloneTextAtlasProvider::ResolveDefaultFont() const { - ImFontAtlas* atlas = ResolveAtlas(); - if (atlas == nullptr) { - return nullptr; - } - - if (IsFontOwnedByAtlas(m_defaultFont, atlas)) { - return m_defaultFont; - } - - return atlas->Fonts.empty() ? nullptr : atlas->Fonts[0]; -} - } // namespace XCUIBackend } // namespace Editor } // namespace XCEngine diff --git a/new_editor/src/XCUIBackend/XCUIStandaloneTextAtlasProvider.h b/new_editor/src/XCUIBackend/XCUIStandaloneTextAtlasProvider.h index 2a6baff2..acf8c19f 100644 --- a/new_editor/src/XCUIBackend/XCUIStandaloneTextAtlasProvider.h +++ b/new_editor/src/XCUIBackend/XCUIStandaloneTextAtlasProvider.h @@ -3,7 +3,7 @@ #include "IXCUITextAtlasProvider.h" #include "XCUIEditorFontSetup.h" -#include +#include namespace XCEngine { namespace Editor { @@ -12,6 +12,7 @@ namespace XCUIBackend { class XCUIStandaloneTextAtlasProvider final : public IXCUITextAtlasProvider { public: XCUIStandaloneTextAtlasProvider(); + ~XCUIStandaloneTextAtlasProvider(); XCUIStandaloneTextAtlasProvider(const XCUIStandaloneTextAtlasProvider&) = delete; XCUIStandaloneTextAtlasProvider& operator=(const XCUIStandaloneTextAtlasProvider&) = delete; @@ -28,13 +29,10 @@ public: bool GetBakedFontInfo(FontHandle font, float fontSize, BakedFontInfo& outInfo) const override; bool FindGlyph(FontHandle font, float fontSize, std::uint32_t codepoint, GlyphInfo& outInfo) const override; -private: - ::ImFontAtlas* ResolveAtlas() const; - ::ImFont* ResolveDefaultFont() const; + struct Impl; - mutable ::ImFontAtlas m_atlas = {}; - ::ImFont* m_defaultFont = nullptr; - bool m_ready = false; +private: + std::unique_ptr m_impl; }; } // namespace XCUIBackend diff --git a/tests/NewEditor/CMakeLists.txt b/tests/NewEditor/CMakeLists.txt index 34dab03c..8029ea4f 100644 --- a/tests/NewEditor/CMakeLists.txt +++ b/tests/NewEditor/CMakeLists.txt @@ -745,18 +745,10 @@ else() endif() if(EXISTS "${NEW_EDITOR_STANDALONE_TEXT_ATLAS_PROVIDER_HEADER}" AND - EXISTS "${NEW_EDITOR_STANDALONE_TEXT_ATLAS_PROVIDER_SOURCE}" AND - EXISTS "${NEW_EDITOR_FONT_SETUP_HEADER}" AND - EXISTS "${NEW_EDITOR_FONT_SETUP_SOURCE}" AND - EXISTS "${CMAKE_BINARY_DIR}/_deps/imgui-src/imgui.cpp") + EXISTS "${NEW_EDITOR_STANDALONE_TEXT_ATLAS_PROVIDER_SOURCE}") add_executable(new_editor_xcui_standalone_text_atlas_provider_tests test_xcui_standalone_text_atlas_provider.cpp - ${NEW_EDITOR_FONT_SETUP_SOURCE} ${NEW_EDITOR_STANDALONE_TEXT_ATLAS_PROVIDER_SOURCE} - ${CMAKE_BINARY_DIR}/_deps/imgui-src/imgui.cpp - ${CMAKE_BINARY_DIR}/_deps/imgui-src/imgui_draw.cpp - ${CMAKE_BINARY_DIR}/_deps/imgui-src/imgui_tables.cpp - ${CMAKE_BINARY_DIR}/_deps/imgui-src/imgui_widgets.cpp ) xcengine_configure_new_editor_test_target(new_editor_xcui_standalone_text_atlas_provider_tests) @@ -767,19 +759,18 @@ if(EXISTS "${NEW_EDITOR_STANDALONE_TEXT_ATLAS_PROVIDER_HEADER}" AND GTest::gtest GTest::gtest_main user32 + gdi32 comdlg32 ) target_include_directories(new_editor_xcui_standalone_text_atlas_provider_tests PRIVATE ${CMAKE_SOURCE_DIR}/engine/include ${CMAKE_SOURCE_DIR}/new_editor/src - ${CMAKE_BINARY_DIR}/_deps/imgui-src - ${CMAKE_BINARY_DIR}/_deps/imgui-src/backends ) xcengine_discover_new_editor_gtests(new_editor_xcui_standalone_text_atlas_provider_tests) else() - message(STATUS "Skipping new_editor_xcui_standalone_text_atlas_provider_tests because standalone atlas provider, font setup, or ImGui sources are missing.") + message(STATUS "Skipping new_editor_xcui_standalone_text_atlas_provider_tests because standalone atlas provider files are missing.") endif() if(EXISTS "${NEW_EDITOR_RHI_COMMAND_COMPILER_HEADER}" AND diff --git a/tests/NewEditor/test_xcui_standalone_text_atlas_provider.cpp b/tests/NewEditor/test_xcui_standalone_text_atlas_provider.cpp index ff7c7fc6..87406463 100644 --- a/tests/NewEditor/test_xcui_standalone_text_atlas_provider.cpp +++ b/tests/NewEditor/test_xcui_standalone_text_atlas_provider.cpp @@ -11,12 +11,15 @@ using XCEngine::Editor::XCUIBackend::XCUIStandaloneTextAtlasProvider; TEST(XCUIStandaloneTextAtlasProviderTest, BuildsDefaultEditorAtlasWithoutImGuiContext) { XCUIStandaloneTextAtlasProvider provider = {}; - IXCUITextAtlasProvider::AtlasTextureView atlasView = {}; + IXCUITextAtlasProvider::AtlasTextureView alphaView = {}; ASSERT_TRUE(provider.IsReady()); ASSERT_TRUE(provider.GetAtlasTextureView(IXCUITextAtlasProvider::PixelFormat::RGBA32, atlasView)); + ASSERT_TRUE(provider.GetAtlasTextureView(IXCUITextAtlasProvider::PixelFormat::Alpha8, alphaView)); EXPECT_TRUE(atlasView.IsValid()); + EXPECT_TRUE(alphaView.IsValid()); EXPECT_EQ(atlasView.format, IXCUITextAtlasProvider::PixelFormat::RGBA32); + EXPECT_EQ(alphaView.format, IXCUITextAtlasProvider::PixelFormat::Alpha8); EXPECT_GT(provider.GetFontCount(), 0u); } @@ -42,4 +45,113 @@ TEST(XCUIStandaloneTextAtlasProviderTest, ExposesDefaultFontMetricsAndGlyphs) { EXPECT_GE(glyphInfo.v1, glyphInfo.v0); } +TEST(XCUIStandaloneTextAtlasProviderTest, RebuildsAfterResetAndRejectsQueriesWhileUnready) { + XCUIStandaloneTextAtlasProvider provider = {}; + const IXCUITextAtlasProvider::FontHandle defaultFont = provider.GetDefaultFont(); + ASSERT_TRUE(defaultFont.IsValid()); + + provider.Reset(); + + IXCUITextAtlasProvider::AtlasTextureView atlasView = {}; + IXCUITextAtlasProvider::FontInfo fontInfo = {}; + IXCUITextAtlasProvider::BakedFontInfo bakedFontInfo = {}; + IXCUITextAtlasProvider::GlyphInfo glyphInfo = {}; + + EXPECT_FALSE(provider.IsReady()); + EXPECT_FALSE(provider.GetAtlasTextureView(IXCUITextAtlasProvider::PixelFormat::RGBA32, atlasView)); + EXPECT_EQ(provider.GetFontCount(), 0u); + EXPECT_FALSE(provider.GetDefaultFont().IsValid()); + EXPECT_FALSE(provider.GetFontInfo(defaultFont, fontInfo)); + EXPECT_FALSE(provider.GetBakedFontInfo(defaultFont, 18.0f, bakedFontInfo)); + EXPECT_FALSE(provider.FindGlyph(defaultFont, 18.0f, static_cast('A'), glyphInfo)); + + ASSERT_TRUE(provider.RebuildDefaultEditorAtlas()); + ASSERT_TRUE(provider.IsReady()); + ASSERT_TRUE(provider.GetAtlasTextureView(IXCUITextAtlasProvider::PixelFormat::RGBA32, atlasView)); + EXPECT_TRUE(atlasView.IsValid()); +} + +TEST(XCUIStandaloneTextAtlasProviderTest, ResolvesGlyphsForNonNominalRequestedFontSizes) { + XCUIStandaloneTextAtlasProvider provider = {}; + const IXCUITextAtlasProvider::FontHandle defaultFont = provider.GetDefaultFont(); + ASSERT_TRUE(defaultFont.IsValid()); + + IXCUITextAtlasProvider::BakedFontInfo bakedFontInfo = {}; + ASSERT_TRUE(provider.GetBakedFontInfo(defaultFont, 13.0f, bakedFontInfo)); + EXPECT_GT(bakedFontInfo.lineHeight, 0.0f); + EXPECT_GT(bakedFontInfo.rasterizerDensity, 0.0f); + + IXCUITextAtlasProvider::GlyphInfo glyphInfo = {}; + ASSERT_TRUE(provider.FindGlyph(defaultFont, 13.0f, static_cast('A'), glyphInfo)); + EXPECT_EQ(glyphInfo.requestedCodepoint, static_cast('A')); + EXPECT_GT(glyphInfo.advanceX, 0.0f); + EXPECT_GE(glyphInfo.u1, glyphInfo.u0); + EXPECT_GE(glyphInfo.v1, glyphInfo.v0); +} + +TEST(XCUIStandaloneTextAtlasProviderTest, LazilyAddsGlyphsOutsideThePrebakedAsciiRanges) { + XCUIStandaloneTextAtlasProvider provider = {}; + const IXCUITextAtlasProvider::FontHandle defaultFont = provider.GetDefaultFont(); + ASSERT_TRUE(defaultFont.IsValid()); + + IXCUITextAtlasProvider::AtlasTextureView beforeView = {}; + ASSERT_TRUE(provider.GetAtlasTextureView(IXCUITextAtlasProvider::PixelFormat::RGBA32, beforeView)); + + constexpr std::uint32_t dynamicCodepoint = 0x4E2Du; + IXCUITextAtlasProvider::GlyphInfo glyphInfo = {}; + ASSERT_TRUE(provider.FindGlyph(defaultFont, 16.0f, dynamicCodepoint, glyphInfo)); + + IXCUITextAtlasProvider::AtlasTextureView refreshedView = {}; + ASSERT_TRUE(provider.GetAtlasTextureView(IXCUITextAtlasProvider::PixelFormat::RGBA32, refreshedView)); + EXPECT_EQ(refreshedView.atlasStorageKey, beforeView.atlasStorageKey); + EXPECT_EQ(glyphInfo.requestedCodepoint, dynamicCodepoint); + + if (refreshedView.pixelDataKey != beforeView.pixelDataKey) { + EXPECT_EQ(glyphInfo.resolvedCodepoint, dynamicCodepoint); + EXPECT_GT(glyphInfo.advanceX, 0.0f); + EXPECT_TRUE(glyphInfo.visible); + + IXCUITextAtlasProvider::GlyphInfo secondLookup = {}; + ASSERT_TRUE(provider.FindGlyph(defaultFont, 16.0f, dynamicCodepoint, secondLookup)); + EXPECT_EQ(secondLookup.requestedCodepoint, dynamicCodepoint); + EXPECT_EQ(secondLookup.resolvedCodepoint, dynamicCodepoint); + + IXCUITextAtlasProvider::AtlasTextureView secondView = {}; + ASSERT_TRUE(provider.GetAtlasTextureView(IXCUITextAtlasProvider::PixelFormat::RGBA32, secondView)); + EXPECT_EQ(secondView.pixelDataKey, refreshedView.pixelDataKey); + return; + } + + if (glyphInfo.resolvedCodepoint != dynamicCodepoint) { + EXPECT_EQ(glyphInfo.resolvedCodepoint, static_cast('?')); + EXPECT_GT(glyphInfo.advanceX, 0.0f); + EXPECT_TRUE(glyphInfo.visible); + return; + } + + if (!glyphInfo.visible) { + EXPECT_GE(glyphInfo.advanceX, 0.0f); + return; + } + + if (glyphInfo.resolvedCodepoint == dynamicCodepoint) { + EXPECT_NE(refreshedView.pixelDataKey, beforeView.pixelDataKey); + } + EXPECT_GT(glyphInfo.advanceX, 0.0f); + EXPECT_TRUE(glyphInfo.visible); +} + +TEST(XCUIStandaloneTextAtlasProviderTest, MissingGlyphFallsBackToQuestionMarkWhenFontCannotRasterizeIt) { + XCUIStandaloneTextAtlasProvider provider = {}; + const IXCUITextAtlasProvider::FontHandle defaultFont = provider.GetDefaultFont(); + ASSERT_TRUE(defaultFont.IsValid()); + + IXCUITextAtlasProvider::GlyphInfo glyphInfo = {}; + ASSERT_TRUE(provider.FindGlyph(defaultFont, 16.0f, 0x10FFFFu, glyphInfo)); + EXPECT_EQ(glyphInfo.requestedCodepoint, 0x10FFFFu); + EXPECT_EQ(glyphInfo.resolvedCodepoint, static_cast('?')); + EXPECT_GT(glyphInfo.advanceX, 0.0f); + EXPECT_TRUE(glyphInfo.visible); +} + } // namespace