De-ImGui XCUI standalone text atlas provider

This commit is contained in:
2026-04-05 15:16:15 +08:00
parent b05e76de0c
commit 6fd3ed434d
5 changed files with 879 additions and 133 deletions

View File

@@ -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.

View File

@@ -1,6 +1,21 @@
#include "XCUIBackend/XCUIStandaloneTextAtlasProvider.h"
#include <imgui_internal.h>
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <windows.h>
#include <wingdi.h>
#include <algorithm>
#include <array>
#include <cmath>
#include <cstddef>
#include <cstdint>
#include <limits>
#include <memory>
#include <unordered_map>
#include <utility>
#include <vector>
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<std::size_t>::max)();
constexpr wchar_t kPrimaryFontFace[] = L"Segoe UI";
constexpr wchar_t kFallbackFontFace[] = L"Microsoft YaHei";
constexpr std::array<int, 5> kSupportedFontSizes = { 13, 14, 16, 18, 20 };
constexpr std::array<std::pair<std::uint32_t, std::uint32_t>, 2> kPrebakedCodepointRanges = {{
{ 0x0020u, 0x007Eu },
{ 0x00A0u, 0x00FFu },
}};
struct CachedPixelBuffer {
std::vector<unsigned char> 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<unsigned char> alpha = {};
};
struct BakedFontRecord {
int sizeKey = 0;
IXCUITextAtlasProvider::BakedFontInfo metrics = {};
std::unordered_map<std::uint32_t, IXCUITextAtlasProvider::GlyphInfo> glyphs = {};
};
IXCUITextAtlasProvider::FontHandle MakeDefaultFontHandle() {
IXCUITextAtlasProvider::FontHandle handle = {};
handle.value = reinterpret_cast<std::uintptr_t>(font);
handle.value = 1u;
return handle;
}
ImFont* ResolveFontHandle(IXCUITextAtlasProvider::FontHandle handle) {
return reinterpret_cast<ImFont*>(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<std::uint64_t>(static_cast<std::uint32_t>(sizeKey)) << 32u) |
static_cast<std::uint64_t>(codepoint);
}
bool IsFontOwnedByAtlas(const ImFont* font, const ImFontAtlas* atlas) {
return font != nullptr && atlas != nullptr && font->OwnerAtlas == atlas;
void AppendCodepointRange(
std::vector<std::uint32_t>& outCodepoints,
std::uint32_t first,
std::uint32_t last) {
if (first > last) {
return;
}
ImFontBaked* ResolveBakedFont(
IXCUITextAtlasProvider::FontHandle fontHandle,
ImFontAtlas* atlas,
float requestedFontSize) {
ImFont* font = ResolveFontHandle(fontHandle);
if (!IsFontOwnedByAtlas(font, atlas)) {
outCodepoints.reserve(outCodepoints.size() + static_cast<std::size_t>(last - first + 1u));
for (std::uint32_t codepoint = first; codepoint <= last; ++codepoint) {
outCodepoints.push_back(codepoint);
}
}
std::vector<std::uint32_t> BuildPrebakedCodepointSet() {
std::vector<std::uint32_t> 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<WCHAR>(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<float>(m_lineMetrics.tmHeight);
info.ascent = static_cast<float>(m_lineMetrics.tmAscent);
info.descent = static_cast<float>(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<WCHAR>(codepoint);
const BOOL extentResult = ::GetTextExtentPoint32W(m_dc, &wideCodepoint, 1, &textExtent);
::SelectObject(m_dc, previous);
outRecord.visible = false;
outRecord.advanceX = extentResult != FALSE
? static_cast<float>(textExtent.cx)
: static_cast<float>(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<UINT>(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<float>(glyphMetrics.gmCellIncX);
outRecord.bitmapWidth = static_cast<int>(glyphMetrics.gmBlackBoxX);
outRecord.bitmapHeight = static_cast<int>(glyphMetrics.gmBlackBoxY);
outRecord.x0 = static_cast<float>(glyphMetrics.gmptGlyphOrigin.x);
outRecord.y0 = static_cast<float>(m_lineMetrics.tmAscent - glyphMetrics.gmptGlyphOrigin.y);
outRecord.x1 = outRecord.x0 + static_cast<float>(glyphMetrics.gmBlackBoxX);
outRecord.y1 = outRecord.y0 + static_cast<float>(glyphMetrics.gmBlackBoxY);
if (!outRecord.visible) {
return true;
}
std::vector<unsigned char> bitmapBuffer(bufferSize);
previous = ::SelectObject(m_dc, resolvedFont);
const DWORD writeResult = ::GetGlyphOutlineW(
m_dc,
static_cast<UINT>(codepoint),
GGO_GRAY8_BITMAP,
&glyphMetrics,
bufferSize,
bitmapBuffer.data(),
&transform);
::SelectObject(m_dc, previous);
if (writeResult == GDI_ERROR) {
return false;
}
const int sourceStride = (static_cast<int>(glyphMetrics.gmBlackBoxX) + 3) & ~3;
const std::size_t requiredBytes =
static_cast<std::size_t>(sourceStride) *
static_cast<std::size_t>((std::max)(0, outRecord.bitmapHeight));
if (bitmapBuffer.size() < requiredBytes) {
return false;
}
outRecord.alpha.resize(
static_cast<std::size_t>(outRecord.bitmapWidth) *
static_cast<std::size_t>(outRecord.bitmapHeight));
for (int y = 0; y < outRecord.bitmapHeight; ++y) {
const unsigned char* sourceRow = bitmapBuffer.data() + static_cast<std::size_t>(y * sourceStride);
unsigned char* destRow = outRecord.alpha.data() +
static_cast<std::size_t>(y * outRecord.bitmapWidth);
for (int x = 0; x < outRecord.bitmapWidth; ++x) {
const unsigned int value = static_cast<unsigned int>(sourceRow[x]);
destRow[x] = static_cast<unsigned char>((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<std::uintptr_t>(pixels.bytes.data()) ^
static_cast<std::uintptr_t>(generation * 1315423911ull);
}
return font->GetFontBaked(resolvedFontSize);
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<float>(lhs));
const float rhsDistance = std::fabs(targetSize - static_cast<float>(rhs));
return lhsDistance < rhsDistance;
});
return bestIt != kSupportedFontSizes.end() ? *bestIt : static_cast<int>(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;
}
outInfo.u0 = static_cast<float>(glyph.atlasX) / static_cast<float>(atlasWidth);
outInfo.v0 = static_cast<float>(glyph.atlasY) / static_cast<float>(atlasHeight);
outInfo.u1 = static_cast<float>(glyph.atlasX + glyph.bitmapWidth) / static_cast<float>(atlasWidth);
outInfo.v1 = static_cast<float>(glyph.atlasY + glyph.bitmapHeight) / static_cast<float>(atlasHeight);
return true;
}
std::vector<std::size_t> BuildVisiblePackOrder(const std::vector<GlyphRecord>& glyphs) {
std::vector<std::size_t> 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<GlyphRecord>& 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<std::size_t> 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<int, BakedFontRecord> bakedFonts = {};
std::vector<GlyphRecord> glyphRecords = {};
std::unordered_map<std::uint64_t, std::size_t> 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<std::size_t>(chosenAtlasWidth) * static_cast<std::size_t>(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<std::size_t>(y * glyph.bitmapWidth);
unsigned char* destRow = impl.alpha8Pixels.bytes.data() +
static_cast<std::size_t>((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<std::size_t>(chosenAtlasWidth) *
static_cast<std::size_t>(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<Impl>()) {
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<Impl>();
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<std::uint32_t> 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<std::uintptr_t>(atlas);
outView.pixelDataKey = reinterpret_cast<std::uintptr_t>(pixels);
outView.atlasStorageKey = reinterpret_cast<std::uintptr_t>(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<std::size_t>(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<std::size_t>(atlas->Fonts.Size)) {
return {};
}
return MakeFontHandle(atlas->Fonts[static_cast<int>(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,51 +795,46 @@ 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<ImWchar>(codepoint));
if (glyph == nullptr) {
return false;
}
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;
const auto glyphIt = bakedFont->glyphs.find(codepoint);
if (glyphIt != bakedFont->glyphs.end()) {
outInfo = glyphIt->second;
return true;
}
::ImFontAtlas* XCUIStandaloneTextAtlasProvider::ResolveAtlas() const {
return m_ready ? &m_atlas : nullptr;
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;
}
}
}
::ImFont* XCUIStandaloneTextAtlasProvider::ResolveDefaultFont() const {
ImFontAtlas* atlas = ResolveAtlas();
if (atlas == nullptr) {
return nullptr;
const BakedFontRecord* fallbackBakedFont = FindBakedFont(*m_impl, sizeKey);
if (fallbackBakedFont == nullptr) {
return false;
}
if (IsFontOwnedByAtlas(m_defaultFont, atlas)) {
return m_defaultFont;
const auto fallbackGlyphIt = fallbackBakedFont->glyphs.find(static_cast<std::uint32_t>('?'));
if (fallbackGlyphIt == fallbackBakedFont->glyphs.end()) {
return false;
}
return atlas->Fonts.empty() ? nullptr : atlas->Fonts[0];
outInfo = fallbackGlyphIt->second;
outInfo.requestedCodepoint = codepoint;
return true;
}
} // namespace XCUIBackend

View File

@@ -3,7 +3,7 @@
#include "IXCUITextAtlasProvider.h"
#include "XCUIEditorFontSetup.h"
#include <imgui.h>
#include <memory>
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<Impl> m_impl;
};
} // namespace XCUIBackend

View File

@@ -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

View File

@@ -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<std::uint32_t>('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<std::uint32_t>('A'), glyphInfo));
EXPECT_EQ(glyphInfo.requestedCodepoint, static_cast<std::uint32_t>('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<std::uint32_t>('?'));
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<std::uint32_t>('?'));
EXPECT_GT(glyphInfo.advanceX, 0.0f);
EXPECT_TRUE(glyphInfo.visible);
}
} // namespace