From dc247ab58d85fdf84a55b391194353f44d3e57c4 Mon Sep 17 00:00:00 2001 From: Aiden Date: Thu, 21 May 2026 15:45:13 +1000 Subject: [PATCH] Golden rules change --- .../runtime/RuntimeTextTextureCache.cpp | 208 ++---------------- src/render/runtime/RuntimeTextTextureCache.h | 2 - src/runtime/FontAtlasBuilder.cpp | 193 ++++++++++++++++ src/runtime/FontAtlasBuilder.h | 24 ++ 4 files changed, 237 insertions(+), 190 deletions(-) diff --git a/src/render/runtime/RuntimeTextTextureCache.cpp b/src/render/runtime/RuntimeTextTextureCache.cpp index 07e02d8..b9029a1 100644 --- a/src/render/runtime/RuntimeTextTextureCache.cpp +++ b/src/render/runtime/RuntimeTextTextureCache.cpp @@ -1,13 +1,7 @@ #include "RuntimeTextTextureCache.h" -#include "../../runtime/RuntimeJson.h" - #include #include -#include -#include -#include -#include namespace { @@ -15,45 +9,6 @@ constexpr GLuint kFirstTextTextureUnit = 8; constexpr unsigned kTextTextureHeight = 256; constexpr unsigned kTextTexturePadding = 16; constexpr double kFontPixelsPerEm = 192.0; - -std::string ReadTextFile(const std::filesystem::path& path) -{ - std::ifstream input(path, std::ios::binary); - if (!input) - return std::string(); - std::ostringstream buffer; - buffer << input.rdbuf(); - return buffer.str(); -} - -const JsonValue* FindObjectValue(const JsonValue& object, const std::string& key) -{ - return object.isObject() ? object.find(key) : nullptr; -} - -double NumberMember(const JsonValue& object, const std::string& key, double fallback = 0.0) -{ - const JsonValue* value = FindObjectValue(object, key); - return value && value->isNumber() ? value->asNumber(fallback) : fallback; -} - -struct ComThreadGuard -{ - ~ComThreadGuard() - { - if (initialized) - CoUninitialize(); - } - - bool Initialize() - { - const HRESULT result = CoInitializeEx(nullptr, COINIT_MULTITHREADED); - initialized = SUCCEEDED(result); - return SUCCEEDED(result) || result == RPC_E_CHANGED_MODE; - } - - bool initialized = false; -}; } RuntimeTextTextureCache::~RuntimeTextTextureCache() @@ -173,155 +128,32 @@ void RuntimeTextTextureCache::AssignSamplerUniforms(GLuint program, const Runtim bool RuntimeTextTextureCache::LoadAtlas(const RenderCadenceCompositor::FontAtlasBuildOutput& output, Atlas& atlas, std::string& error) const { atlas.fontId = output.fontId; - if (!LoadAtlasJson(output, atlas, error)) - return false; - if (!LoadAtlasImage(output, atlas, error)) - return false; - if (atlas.width == 0 || atlas.height == 0 || atlas.rgbaPixels.empty()) + atlas.width = output.width; + atlas.height = output.height; + atlas.ascender = output.ascender; + atlas.descender = output.descender; + atlas.lineHeight = output.lineHeight; + atlas.rgbaPixels = output.rgbaPixels; + for (const auto& entry : output.glyphsByCodepoint) { - error = "Font atlas image is empty for font '" + output.fontId + "'."; - return false; - } - return true; -} - -bool RuntimeTextTextureCache::LoadAtlasJson(const RenderCadenceCompositor::FontAtlasBuildOutput& output, Atlas& atlas, std::string& error) const -{ - const std::string jsonText = ReadTextFile(output.jsonPath); - if (jsonText.empty()) - { - error = "Could not read font atlas json: " + output.jsonPath.string(); - return false; - } - - JsonValue root; - if (!ParseJson(jsonText, root, error)) - return false; - - const JsonValue* metrics = FindObjectValue(root, "metrics"); - if (metrics) - { - atlas.ascender = NumberMember(*metrics, "ascender", atlas.ascender); - atlas.descender = NumberMember(*metrics, "descender", atlas.descender); - atlas.lineHeight = NumberMember(*metrics, "lineHeight", atlas.lineHeight); - } - - const JsonValue* glyphs = FindObjectValue(root, "glyphs"); - if (!glyphs || !glyphs->isArray()) - { - error = "Font atlas json has no glyph array: " + output.jsonPath.string(); - return false; - } - - for (const JsonValue& glyphJson : glyphs->asArray()) - { - if (!glyphJson.isObject()) - continue; - - const unsigned codepoint = static_cast(NumberMember(glyphJson, "unicode", 0.0)); Glyph glyph; - glyph.advance = NumberMember(glyphJson, "advance", 0.0); - - const JsonValue* planeBounds = FindObjectValue(glyphJson, "planeBounds"); - const JsonValue* atlasBounds = FindObjectValue(glyphJson, "atlasBounds"); - if (planeBounds && atlasBounds) - { - glyph.planeLeft = NumberMember(*planeBounds, "left", 0.0); - glyph.planeTop = NumberMember(*planeBounds, "top", 0.0); - glyph.planeRight = NumberMember(*planeBounds, "right", 0.0); - glyph.planeBottom = NumberMember(*planeBounds, "bottom", 0.0); - glyph.atlasLeft = NumberMember(*atlasBounds, "left", 0.0); - glyph.atlasTop = NumberMember(*atlasBounds, "top", 0.0); - glyph.atlasRight = NumberMember(*atlasBounds, "right", 0.0); - glyph.atlasBottom = NumberMember(*atlasBounds, "bottom", 0.0); - glyph.hasBounds = true; - } - - atlas.glyphsByCodepoint[codepoint] = glyph; + glyph.advance = entry.second.advance; + glyph.planeLeft = entry.second.planeLeft; + glyph.planeTop = entry.second.planeTop; + glyph.planeRight = entry.second.planeRight; + glyph.planeBottom = entry.second.planeBottom; + glyph.atlasLeft = entry.second.atlasLeft; + glyph.atlasTop = entry.second.atlasTop; + glyph.atlasRight = entry.second.atlasRight; + glyph.atlasBottom = entry.second.atlasBottom; + glyph.hasBounds = entry.second.hasBounds; + atlas.glyphsByCodepoint[entry.first] = glyph; } - - error.clear(); - return true; -} - -bool RuntimeTextTextureCache::LoadAtlasImage(const RenderCadenceCompositor::FontAtlasBuildOutput& output, Atlas& atlas, std::string& error) const -{ - ComThreadGuard comGuard; - if (!comGuard.Initialize()) + if (atlas.width == 0 || atlas.height == 0 || atlas.rgbaPixels.empty() || atlas.glyphsByCodepoint.empty()) { - error = "Could not initialize COM for font atlas PNG loading."; + error = "Prepared font atlas data is empty for font '" + output.fontId + "'."; return false; } - - Microsoft::WRL::ComPtr factory; - HRESULT result = CoCreateInstance( - CLSID_WICImagingFactory, - nullptr, - CLSCTX_INPROC_SERVER, - IID_PPV_ARGS(factory.GetAddressOf())); - if (FAILED(result)) - { - error = "Could not create WIC imaging factory for font atlas PNG loading."; - return false; - } - - Microsoft::WRL::ComPtr decoder; - result = factory->CreateDecoderFromFilename( - output.imagePath.wstring().c_str(), - nullptr, - GENERIC_READ, - WICDecodeMetadataCacheOnLoad, - decoder.GetAddressOf()); - if (FAILED(result)) - { - error = "Could not decode font atlas PNG: " + output.imagePath.string(); - return false; - } - - Microsoft::WRL::ComPtr frame; - result = decoder->GetFrame(0, frame.GetAddressOf()); - if (FAILED(result)) - { - error = "Could not read font atlas PNG frame: " + output.imagePath.string(); - return false; - } - - Microsoft::WRL::ComPtr converter; - result = factory->CreateFormatConverter(converter.GetAddressOf()); - if (FAILED(result)) - { - error = "Could not create WIC format converter for font atlas PNG."; - return false; - } - - result = converter->Initialize( - frame.Get(), - GUID_WICPixelFormat32bppRGBA, - WICBitmapDitherTypeNone, - nullptr, - 0.0, - WICBitmapPaletteTypeCustom); - if (FAILED(result)) - { - error = "Could not convert font atlas PNG to RGBA."; - return false; - } - - UINT width = 0; - UINT height = 0; - converter->GetSize(&width, &height); - atlas.width = static_cast(width); - atlas.height = static_cast(height); - atlas.rgbaPixels.assign(static_cast(atlas.width) * atlas.height * 4u, 0); - - const UINT stride = width * 4u; - result = converter->CopyPixels(nullptr, stride, static_cast(atlas.rgbaPixels.size()), atlas.rgbaPixels.data()); - if (FAILED(result)) - { - error = "Could not copy font atlas PNG pixels."; - return false; - } - error.clear(); return true; } diff --git a/src/render/runtime/RuntimeTextTextureCache.h b/src/render/runtime/RuntimeTextTextureCache.h index 9ecf761..e1db6ec 100644 --- a/src/render/runtime/RuntimeTextTextureCache.h +++ b/src/render/runtime/RuntimeTextTextureCache.h @@ -63,8 +63,6 @@ private: }; bool LoadAtlas(const RenderCadenceCompositor::FontAtlasBuildOutput& output, Atlas& atlas, std::string& error) const; - bool LoadAtlasJson(const RenderCadenceCompositor::FontAtlasBuildOutput& output, Atlas& atlas, std::string& error) const; - bool LoadAtlasImage(const RenderCadenceCompositor::FontAtlasBuildOutput& output, Atlas& atlas, std::string& error) const; bool EnsureTextTexture(TextTexture& texture); std::vector ComposeTextTexture(const Atlas& atlas, const TextTexture& texture, const std::string& text, unsigned& width, unsigned& height, unsigned& liveWidth) const; const Atlas* FindAtlas(const std::string& fontId) const; diff --git a/src/runtime/FontAtlasBuilder.cpp b/src/runtime/FontAtlasBuilder.cpp index aab69e6..f3e69ec 100644 --- a/src/runtime/FontAtlasBuilder.cpp +++ b/src/runtime/FontAtlasBuilder.cpp @@ -1,12 +1,15 @@ #include "FontAtlasBuilder.h" #include "NativeHandles.h" +#include "RuntimeJson.h" #include #include #include #include #include +#include +#include #include namespace RenderCadenceCompositor @@ -24,6 +27,45 @@ std::string NumberText(double value) stream << value; return stream.str(); } + +std::string ReadTextFile(const std::filesystem::path& path) +{ + std::ifstream input(path, std::ios::binary); + if (!input) + return std::string(); + std::ostringstream buffer; + buffer << input.rdbuf(); + return buffer.str(); +} + +const JsonValue* FindObjectValue(const JsonValue& object, const std::string& key) +{ + return object.isObject() ? object.find(key) : nullptr; +} + +double NumberMember(const JsonValue& object, const std::string& key, double fallback = 0.0) +{ + const JsonValue* value = FindObjectValue(object, key); + return value && value->isNumber() ? value->asNumber(fallback) : fallback; +} + +struct ComThreadGuard +{ + ~ComThreadGuard() + { + if (initialized) + CoUninitialize(); + } + + bool Initialize() + { + const HRESULT result = CoInitializeEx(nullptr, COINIT_MULTITHREADED); + initialized = SUCCEEDED(result); + return SUCCEEDED(result) || result == RPC_E_CHANGED_MODE; + } + + bool initialized = false; +}; } FontAtlasBuilder::FontAtlasBuilder(FontAtlasBuildConfig config) : @@ -111,6 +153,15 @@ bool FontAtlasBuilder::BuildFontAtlas( error = "msdf-atlas-gen did not produce expected atlas outputs for font '" + fontAsset.id + "'."; return false; } + if (!LoadAtlasJson(output, error)) + return false; + if (!LoadAtlasImage(output, error)) + return false; + if (output.width == 0 || output.height == 0 || output.rgbaPixels.empty()) + { + error = "Prepared font atlas image is empty for font '" + fontAsset.id + "'."; + return false; + } error.clear(); return true; @@ -133,6 +184,148 @@ std::filesystem::path FontAtlasBuilder::PackageCacheDirectory(const ShaderPackag return mConfig.cacheRoot / SanitizePathToken(shaderPackage.id); } +bool FontAtlasBuilder::LoadAtlasJson(FontAtlasBuildOutput& output, std::string& error) const +{ + const std::string jsonText = ReadTextFile(output.jsonPath); + if (jsonText.empty()) + { + error = "Could not read font atlas json: " + output.jsonPath.string(); + return false; + } + + JsonValue root; + if (!ParseJson(jsonText, root, error)) + return false; + + const JsonValue* metrics = FindObjectValue(root, "metrics"); + if (metrics) + { + output.ascender = NumberMember(*metrics, "ascender", output.ascender); + output.descender = NumberMember(*metrics, "descender", output.descender); + output.lineHeight = NumberMember(*metrics, "lineHeight", output.lineHeight); + } + + const JsonValue* glyphs = FindObjectValue(root, "glyphs"); + if (!glyphs || !glyphs->isArray()) + { + error = "Font atlas json has no glyph array: " + output.jsonPath.string(); + return false; + } + + output.glyphsByCodepoint.clear(); + for (const JsonValue& glyphJson : glyphs->asArray()) + { + if (!glyphJson.isObject()) + continue; + + const unsigned codepoint = static_cast(NumberMember(glyphJson, "unicode", 0.0)); + FontAtlasBuildOutput::Glyph glyph; + glyph.advance = NumberMember(glyphJson, "advance", 0.0); + + const JsonValue* planeBounds = FindObjectValue(glyphJson, "planeBounds"); + const JsonValue* atlasBounds = FindObjectValue(glyphJson, "atlasBounds"); + if (planeBounds && atlasBounds) + { + glyph.planeLeft = NumberMember(*planeBounds, "left", 0.0); + glyph.planeTop = NumberMember(*planeBounds, "top", 0.0); + glyph.planeRight = NumberMember(*planeBounds, "right", 0.0); + glyph.planeBottom = NumberMember(*planeBounds, "bottom", 0.0); + glyph.atlasLeft = NumberMember(*atlasBounds, "left", 0.0); + glyph.atlasTop = NumberMember(*atlasBounds, "top", 0.0); + glyph.atlasRight = NumberMember(*atlasBounds, "right", 0.0); + glyph.atlasBottom = NumberMember(*atlasBounds, "bottom", 0.0); + glyph.hasBounds = true; + } + + output.glyphsByCodepoint[codepoint] = glyph; + } + + error.clear(); + return true; +} + +bool FontAtlasBuilder::LoadAtlasImage(FontAtlasBuildOutput& output, std::string& error) const +{ + ComThreadGuard comGuard; + if (!comGuard.Initialize()) + { + error = "Could not initialize COM for font atlas PNG loading."; + return false; + } + + Microsoft::WRL::ComPtr factory; + HRESULT result = CoCreateInstance( + CLSID_WICImagingFactory, + nullptr, + CLSCTX_INPROC_SERVER, + IID_PPV_ARGS(factory.GetAddressOf())); + if (FAILED(result)) + { + error = "Could not create WIC imaging factory for font atlas PNG loading."; + return false; + } + + Microsoft::WRL::ComPtr decoder; + result = factory->CreateDecoderFromFilename( + output.imagePath.wstring().c_str(), + nullptr, + GENERIC_READ, + WICDecodeMetadataCacheOnLoad, + decoder.GetAddressOf()); + if (FAILED(result)) + { + error = "Could not decode font atlas PNG: " + output.imagePath.string(); + return false; + } + + Microsoft::WRL::ComPtr frame; + result = decoder->GetFrame(0, frame.GetAddressOf()); + if (FAILED(result)) + { + error = "Could not read font atlas PNG frame: " + output.imagePath.string(); + return false; + } + + Microsoft::WRL::ComPtr converter; + result = factory->CreateFormatConverter(converter.GetAddressOf()); + if (FAILED(result)) + { + error = "Could not create WIC format converter for font atlas PNG."; + return false; + } + + result = converter->Initialize( + frame.Get(), + GUID_WICPixelFormat32bppRGBA, + WICBitmapDitherTypeNone, + nullptr, + 0.0, + WICBitmapPaletteTypeCustom); + if (FAILED(result)) + { + error = "Could not convert font atlas PNG to RGBA."; + return false; + } + + UINT width = 0; + UINT height = 0; + converter->GetSize(&width, &height); + output.width = static_cast(width); + output.height = static_cast(height); + output.rgbaPixels.assign(static_cast(output.width) * output.height * 4u, 0); + + const UINT stride = width * 4u; + result = converter->CopyPixels(nullptr, stride, static_cast(output.rgbaPixels.size()), output.rgbaPixels.data()); + if (FAILED(result)) + { + error = "Could not copy font atlas PNG pixels."; + return false; + } + + error.clear(); + return true; +} + std::string FontAtlasBuilder::SanitizePathToken(const std::string& value) { std::string sanitized; diff --git a/src/runtime/FontAtlasBuilder.h b/src/runtime/FontAtlasBuilder.h index 7160c42..45054df 100644 --- a/src/runtime/FontAtlasBuilder.h +++ b/src/runtime/FontAtlasBuilder.h @@ -3,6 +3,7 @@ #include "ShaderTypes.h" #include +#include #include #include @@ -19,9 +20,30 @@ struct FontAtlasBuildConfig struct FontAtlasBuildOutput { + struct Glyph + { + double advance = 0.0; + double planeLeft = 0.0; + double planeTop = 0.0; + double planeRight = 0.0; + double planeBottom = 0.0; + double atlasLeft = 0.0; + double atlasTop = 0.0; + double atlasRight = 0.0; + double atlasBottom = 0.0; + bool hasBounds = false; + }; + std::string fontId; std::filesystem::path imagePath; std::filesystem::path jsonPath; + unsigned width = 0; + unsigned height = 0; + double ascender = -0.9; + double descender = 0.25; + double lineHeight = 1.2; + std::vector rgbaPixels; + std::map glyphsByCodepoint; }; class FontAtlasBuilder @@ -44,6 +66,8 @@ public: private: std::filesystem::path PackageCacheDirectory(const ShaderPackage& shaderPackage) const; + bool LoadAtlasJson(FontAtlasBuildOutput& output, std::string& error) const; + bool LoadAtlasImage(FontAtlasBuildOutput& output, std::string& error) const; static std::string SanitizePathToken(const std::string& value); static bool RunProcess(const std::string& commandLine, const std::filesystem::path& workingDirectory, std::string& error);