From 74327aa8cdc2f601d316d20d0d12c7c3feebde6c Mon Sep 17 00:00:00 2001 From: fibref Date: Sat, 8 Aug 2026 08:53:20 +0800 Subject: [PATCH 01/10] add MSDFsize to FontAsset --- Source/Editor/Windows/Assets/FontWindow.cs | 7 +++++ .../Content/Upgraders/FontAssetUpgrader.h | 31 +++++++++++++++++-- Source/Engine/ContentImporters/ImportFont.cpp | 3 +- Source/Engine/Render2D/FontAsset.cs | 8 ++--- Source/Engine/Render2D/FontAsset.h | 7 ++++- 5 files changed, 47 insertions(+), 9 deletions(-) diff --git a/Source/Editor/Windows/Assets/FontWindow.cs b/Source/Editor/Windows/Assets/FontWindow.cs index e08a87b05..0f0bc1630 100644 --- a/Source/Editor/Windows/Assets/FontWindow.cs +++ b/Source/Editor/Windows/Assets/FontWindow.cs @@ -1,5 +1,6 @@ // Copyright (c) Wojciech Figat. All rights reserved. +using System; using System.ComponentModel; using FlaxEditor.Content; using FlaxEditor.CustomEditors; @@ -25,6 +26,10 @@ namespace FlaxEditor.Windows.Assets [EditorOrder(5), EditorDisplay("Properties"), Tooltip("The rasterization mode used when generating font atlases.")] public FontRasterMode RasterMode; + [DefaultValue(32.0f)] + [EditorOrder(6), EditorDisplay("Properties"), Tooltip("The font size used when generating MSDF font atlases.")] + public float MSDFSize; + [DefaultValue(FontHinting.Default)] [EditorOrder(10), EditorDisplay("Properties"), Tooltip("The font hinting used when rendering characters.")] public FontHinting Hinting; @@ -47,6 +52,7 @@ namespace FlaxEditor.Windows.Assets { Hinting = Hinting, RasterMode = RasterMode, + MSDFSize = MSDFSize, }; if (AntiAliasing) options.Flags |= FontFlags.AntiAliasing; @@ -63,6 +69,7 @@ namespace FlaxEditor.Windows.Assets Bold = (options.Flags & FontFlags.Bold) == FontFlags.Bold; Italic = (options.Flags & FontFlags.Italic) == FontFlags.Italic; RasterMode = options.RasterMode; + MSDFSize = options.MSDFSize; } } diff --git a/Source/Engine/Content/Upgraders/FontAssetUpgrader.h b/Source/Engine/Content/Upgraders/FontAssetUpgrader.h index 4ca6e6d73..b6e8ec12a 100644 --- a/Source/Engine/Content/Upgraders/FontAssetUpgrader.h +++ b/Source/Engine/Content/Upgraders/FontAssetUpgrader.h @@ -19,12 +19,13 @@ public: const Upgrader upgraders[] = { { 3, 4, &Upgrade_3_To_4 }, + { 4, 5, &Upgrade_4_To_5 }, }; setup(upgraders, ARRAY_COUNT(upgraders)); } private: - struct FontOptionsOld + struct FontOptions3 { FontHinting Hinting; FontFlags Flags; @@ -34,8 +35,8 @@ private: { ASSERT(context.Input.SerializedVersion == 3 && context.Output.SerializedVersion == 4); - FontOptionsOld optionsOld; - Platform::MemoryCopy(&optionsOld, context.Input.CustomData.Get(), sizeof(FontOptionsOld)); + FontOptions3 optionsOld; + Platform::MemoryCopy(&optionsOld, context.Input.CustomData.Get(), sizeof(FontOptions3)); FontOptions options; options.Hinting = optionsOld.Hinting; @@ -45,6 +46,30 @@ private: return CopyChunk(context, 0); } + + struct FontOptions4 + { + FontHinting Hinting; + FontFlags Flags; + FontRasterMode RasterMode; + }; + + static bool Upgrade_4_To_5(AssetMigrationContext& context) + { + ASSERT(context.Input.SerializedVersion == 4 && context.Output.SerializedVersion == 5); + + FontOptions4 optionsOld; + Platform::MemoryCopy(&optionsOld, context.Input.CustomData.Get(), sizeof(FontOptions4)); + + FontOptions options; + options.Hinting = optionsOld.Hinting; + options.Flags = optionsOld.Flags; + options.RasterMode = optionsOld.RasterMode; + options.MSDFSize = 32.0f; + context.Output.CustomData.Copy(&options); + + return CopyChunk(context, 0); + } }; #endif diff --git a/Source/Engine/ContentImporters/ImportFont.cpp b/Source/Engine/ContentImporters/ImportFont.cpp index 964f3907e..b01b36627 100644 --- a/Source/Engine/ContentImporters/ImportFont.cpp +++ b/Source/Engine/ContentImporters/ImportFont.cpp @@ -12,13 +12,14 @@ CreateAssetResult ImportFont::Import(CreateAssetContext& context) { // Base - IMPORT_SETUP(FontAsset, 4); + IMPORT_SETUP(FontAsset, 5); // Setup header FontOptions options; options.Hinting = FontHinting::Default; options.Flags = FontFlags::AntiAliasing; options.RasterMode = FontRasterMode::Bitmap; + options.MSDFSize = 32.0f; context.Data.CustomData.Copy(&options); // Open the file diff --git a/Source/Engine/Render2D/FontAsset.cs b/Source/Engine/Render2D/FontAsset.cs index 624daab5b..f49ae4ebb 100644 --- a/Source/Engine/Render2D/FontAsset.cs +++ b/Source/Engine/Render2D/FontAsset.cs @@ -13,7 +13,7 @@ namespace FlaxEngine /// true if this object has the same value as ; otherwise, false public bool Equals(FontOptions other) { - return Hinting == other.Hinting && Flags == other.Flags && RasterMode == other.RasterMode; + return Hinting == other.Hinting && Flags == other.Flags && RasterMode == other.RasterMode && MSDFSize == other.MSDFSize; } /// @@ -25,7 +25,7 @@ namespace FlaxEngine /// public override int GetHashCode() { - return HashCode.Combine((int)Hinting, (int)Flags, (int)RasterMode); + return HashCode.Combine((int)Hinting, (int)Flags, (int)RasterMode, MSDFSize); } /// @@ -36,7 +36,7 @@ namespace FlaxEngine /// true if has the same value as ; otherwise, false. public static bool operator ==(FontOptions left, FontOptions right) { - return left.Hinting == right.Hinting && left.Flags == right.Flags && left.RasterMode == right.RasterMode; + return left.Hinting == right.Hinting && left.Flags == right.Flags && left.RasterMode == right.RasterMode && left.MSDFSize == right.MSDFSize; } /// @@ -47,7 +47,7 @@ namespace FlaxEngine /// true if has a different value than ; otherwise,false. public static bool operator !=(FontOptions left, FontOptions right) { - return left.Hinting != right.Hinting || left.Flags != right.Flags || left.RasterMode != right.RasterMode; + return left.Hinting != right.Hinting || left.Flags != right.Flags || left.RasterMode != right.RasterMode || left.MSDFSize != right.MSDFSize; } } } diff --git a/Source/Engine/Render2D/FontAsset.h b/Source/Engine/Render2D/FontAsset.h index c3aa141a8..4bf2114e1 100644 --- a/Source/Engine/Render2D/FontAsset.h +++ b/Source/Engine/Render2D/FontAsset.h @@ -105,6 +105,11 @@ API_STRUCT() struct FontOptions /// The font rasterization mode. /// API_FIELD() FontRasterMode RasterMode; + + /// + /// The font size used when generating MSDF font atlases. + /// + API_FIELD() float MSDFSize; }; /// @@ -112,7 +117,7 @@ API_STRUCT() struct FontOptions /// API_CLASS(NoSpawn) class FLAXENGINE_API FontAsset : public BinaryAsset { - DECLARE_BINARY_ASSET_HEADER(FontAsset, 4); + DECLARE_BINARY_ASSET_HEADER(FontAsset, 5); friend Font; private: From f9cb513fb7f5d8c65ab25b6a5fbf5c5d21cf1a3e Mon Sep 17 00:00:00 2001 From: fibref Date: Thu, 13 Aug 2026 10:43:10 +0800 Subject: [PATCH 02/10] handle generation and rendering of MSDF fonts --- Source/Engine/Render2D/Font.cpp | 10 ++++++---- Source/Engine/Render2D/Render2D.cpp | 10 ++++++---- Source/Engine/UI/TextRender.cpp | 2 +- 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/Source/Engine/Render2D/Font.cpp b/Source/Engine/Render2D/Font.cpp index b2e6e8b75..6a5a7127a 100644 --- a/Source/Engine/Render2D/Font.cpp +++ b/Source/Engine/Render2D/Font.cpp @@ -133,7 +133,7 @@ void Font::ProcessText(const StringView& text, ArrayGetOptions().RasterMode == FontRasterMode::MSDF ? _size / _asset->GetOptions().MSDFSize : 1.0f); float boundsWidth = layout.Bounds.GetWidth(); float baseLinesDistance = static_cast(_height) * layout.BaseLinesGapScale * scale; tmpLine.Location = Float2::Zero; @@ -339,7 +339,7 @@ int32 Font::HitTestText(const StringView& text, const Float2& location, const Te Array> lines; ProcessText(text, lines, layout); ASSERT(lines.HasItems()); - float scale = layout.Scale / FontManager::FontScale; + float scale = layout.Scale / FontManager::FontScale * (_asset->GetOptions().RasterMode == FontRasterMode::MSDF ? _size / _asset->GetOptions().MSDFSize : 1.0f); float baseLinesDistance = static_cast(_height) * layout.BaseLinesGapScale * scale; // Offset position to match lines origin space @@ -427,7 +427,7 @@ Float2 Font::GetCharPosition(const StringView& text, int32 index, const TextLayo Array> lines; ProcessText(text, lines, layout); ASSERT(lines.HasItems()); - float scale = layout.Scale / FontManager::FontScale; + float scale = layout.Scale / FontManager::FontScale * (_asset->GetOptions().RasterMode == FontRasterMode::MSDF ? _size / _asset->GetOptions().MSDFSize : 1.0f); float baseLinesDistance = static_cast(_height) * layout.BaseLinesGapScale * scale; // Find line with that position @@ -474,7 +474,9 @@ void Font::FlushFaceSize() const { // Set the character size const FT_Face face = _asset->GetFTFace(); - const FT_Error error = FT_Set_Char_Size(face, 0, ConvertPixelTo26Dot6(_size * FontManager::FontScale), DefaultDPI, DefaultDPI); + FontOptions options = _asset->GetOptions(); + float size = options.RasterMode == FontRasterMode::MSDF ? options.MSDFSize : _size; + const FT_Error error = FT_Set_Char_Size(face, 0, ConvertPixelTo26Dot6(size * FontManager::FontScale), DefaultDPI, DefaultDPI); if (error) { LOG_FT_ERROR(error); diff --git a/Source/Engine/Render2D/Render2D.cpp b/Source/Engine/Render2D/Render2D.cpp index 58f6f208e..efabc4b58 100644 --- a/Source/Engine/Render2D/Render2D.cpp +++ b/Source/Engine/Render2D/Render2D.cpp @@ -1203,7 +1203,8 @@ void Render2D::DrawText(Font* font, const StringView& text, const Color& color, Float2 invAtlasSize = Float2::One; FontCharacterEntry previous; int32 kerning; - float scale = 1.0f / FontManager::FontScale; + FontOptions options = font->GetAsset()->GetOptions(); + float scale = 1.0f / FontManager::FontScale * (options.RasterMode == FontRasterMode::MSDF ? font->GetSize() / options.MSDFSize : 1.0f); const bool enableFallbackFonts = EnumHasAllFlags(Features, RenderingFeatures::FallbackFonts); // Render all characters @@ -1216,7 +1217,7 @@ void Render2D::DrawText(Font* font, const StringView& text, const Color& color, } else { - drawCall.Type = font->GetAsset()->GetOptions().RasterMode == FontRasterMode::MSDF ? DrawCallType::DrawCharMSDF : DrawCallType::DrawChar; + drawCall.Type = options.RasterMode == FontRasterMode::MSDF ? DrawCallType::DrawCharMSDF : DrawCallType::DrawChar; drawCall.AsChar.Mat = nullptr; } Float2 pointer = location; @@ -1318,7 +1319,8 @@ void Render2D::DrawText(Font* font, const StringView& text, const Color& color, Float2 invAtlasSize = Float2::One; FontCharacterEntry previous; int32 kerning; - float scale = layout.Scale / FontManager::FontScale; + FontOptions options = font->GetAsset()->GetOptions(); + float scale = layout.Scale / FontManager::FontScale * (options.RasterMode == FontRasterMode::MSDF ? font->GetSize() / options.MSDFSize : 1.0f); const bool enableFallbackFonts = EnumHasAllFlags(Features, RenderingFeatures::FallbackFonts); // Process text to get lines @@ -1335,7 +1337,7 @@ void Render2D::DrawText(Font* font, const StringView& text, const Color& color, } else { - drawCall.Type = font->GetAsset()->GetOptions().RasterMode == FontRasterMode::MSDF ? DrawCallType::DrawCharMSDF : DrawCallType::DrawChar; + drawCall.Type = options.RasterMode == FontRasterMode::MSDF ? DrawCallType::DrawCharMSDF : DrawCallType::DrawChar; drawCall.AsChar.Mat = nullptr; } for (int32 lineIndex = 0; lineIndex < Lines.Count(); lineIndex++) diff --git a/Source/Engine/UI/TextRender.cpp b/Source/Engine/UI/TextRender.cpp index c2f6789c7..88e5f8b2b 100644 --- a/Source/Engine/UI/TextRender.cpp +++ b/Source/Engine/UI/TextRender.cpp @@ -172,7 +172,7 @@ void TextRender::UpdateLayout() // Pick a font (remove DPI text scale as the text is being placed in the world) auto font = Font->CreateFont(_size); - float scale = _layoutOptions.Scale / FontManager::FontScale; + float scale = _layoutOptions.Scale / FontManager::FontScale * (Font->GetOptions().RasterMode == FontRasterMode::MSDF ? _size / Font->GetOptions().MSDFSize : 1.0f); // Prepare FontTextureAtlas* fontAtlas = nullptr; From f6c1d48f328a46af6434a894f6c95adac6b5e5f4 Mon Sep 17 00:00:00 2001 From: fibref Date: Thu, 13 Aug 2026 20:37:51 +0800 Subject: [PATCH 03/10] improve MSDF fonts caching --- Source/Engine/Render2D/Font.cpp | 19 +---- Source/Engine/Render2D/Font.h | 111 +-------------------------- Source/Engine/Render2D/FontAsset.cpp | 6 +- Source/Engine/Render2D/FontAsset.h | 106 +++++++++++++++++++++++++ 4 files changed, 115 insertions(+), 127 deletions(-) diff --git a/Source/Engine/Render2D/Font.cpp b/Source/Engine/Render2D/Font.cpp index 6a5a7127a..ed3093f35 100644 --- a/Source/Engine/Render2D/Font.cpp +++ b/Source/Engine/Render2D/Font.cpp @@ -13,7 +13,6 @@ Font::Font(FontAsset* parentAsset, float size) : ManagedScriptingObject(SpawnParams(Guid::New(), Font::TypeInitializer)) , _asset(parentAsset) , _size(size) - , _characters(512) { _asset->_fonts.Add(this); @@ -37,13 +36,14 @@ Font::~Font() void Font::GetCharacter(Char c, FontCharacterEntry& result, bool enableFallback) { // Try to get the character or cache it if cannot be found - if (!_characters.TryGet(c, result)) + const auto key = Pair(_asset->GetOptions().RasterMode == FontRasterMode::MSDF ? _asset->GetOptions().MSDFSize : GetSize(), c); + if (!_asset->_characterCache.TryGet(key, result)) { // This thread race condition may happen in editor but in game we usually do all stuff with fonts on main thread (chars caching) ScopeLock lock(_asset->Locker); // Handle situation when more than one thread wants to get the same character - if (_characters.TryGet(c, result)) + if (_asset->_characterCache.TryGet(key, result)) return; // Try to use fallback font if character is missing @@ -69,7 +69,7 @@ void Font::GetCharacter(Char c, FontCharacterEntry& result, bool enableFallback) ASSERT(result.Font); // Add to the dictionary - _characters.Add(c, result); + _asset->_characterCache.Add(key, result); } } @@ -112,17 +112,6 @@ void Font::CacheText(const StringView& text) } } -void Font::Invalidate() -{ - ScopeLock lock(_asset->Locker); - - for (auto i = _characters.Begin(); i.IsNotEnd(); ++i) - { - FontManager::Invalidate(i->Value); - } - _characters.Clear(); -} - void Font::ProcessText(const StringView& text, Array>& outputLines, const TextLayoutOptions& layout) { int32 textLength = text.Length(); diff --git a/Source/Engine/Render2D/Font.h b/Source/Engine/Render2D/Font.h index fc40cc16c..f176bb3f3 100644 --- a/Source/Engine/Render2D/Font.h +++ b/Source/Engine/Render2D/Font.h @@ -11,6 +11,7 @@ class FontAsset; struct FontTextureAtlasSlot; +struct FontCharacterEntry; // The default DPI that engine is using #define DefaultDPI 96 @@ -119,110 +120,6 @@ struct TIsPODType enum { Value = true }; }; -// Font glyph metrics: -// -// xmin xmax -// | | -// |<-------- width -------->| -// | | -// | +-------------------------+----------------- ymax -// | | ggggggggg ggggg | ^ ^ -// | | g:::::::::ggg::::g | | | -// | | g:::::::::::::::::g | | | -// | | g::::::ggggg::::::gg | | | -// | | g:::::g g:::::g | | | -// offsetX -|-------->| g:::::g g:::::g | offsetY | -// | | g:::::g g:::::g | | | -// | | g::::::g g:::::g | | | -// | | g:::::::ggggg:::::g | | | -// | | g::::::::::::::::g | | height -// | | gg::::::::::::::g | | | -// baseline ---*---------|---- gggggggg::::::g-----*-------- | -// / | | g:::::g | | -// origin | | gggggg g:::::g | | -// | | g:::::gg gg:::::g | | -// | | g::::::ggg:::::::g | | -// | | gg:::::::::::::g | | -// | | ggg::::::ggg | | -// | | gggggg | v -// | +-------------------------+----------------- ymin -// | | -// |------------- advanceX ----------->| - -/// -/// The cached font character entry (read for rendering and further processing). -/// -API_STRUCT(NoDefault) struct FLAXENGINE_API FontCharacterEntry -{ - DECLARE_SCRIPTING_TYPE_MINIMAL(FontCharacterEntry); - - /// - /// The character represented by this entry. - /// - API_FIELD() Char Character; - - /// - /// True if entry is valid, otherwise false. - /// - API_FIELD() bool IsValid = false; - - /// - /// The index to a specific texture in the font cache. - /// - API_FIELD() byte TextureIndex; - - /// - /// The left bearing expressed in integer pixels. - /// - API_FIELD() int16 OffsetX; - - /// - /// The top bearing expressed in integer pixels. - /// - API_FIELD() int16 OffsetY; - - /// - /// The amount to advance in X before drawing the next character in a string. - /// - API_FIELD() int16 AdvanceX; - - /// - /// The distance from baseline to glyph top most point. - /// - API_FIELD() int16 BearingY; - - /// - /// The height in pixels of the glyph. - /// - API_FIELD() int16 Height; - - /// - /// The start location of the character in the texture (in texture coordinates space). - /// - API_FIELD() Float2 UV; - - /// - /// The size the character in the texture (in texture coordinates space). - /// - API_FIELD() Float2 UVSize; - - /// - /// The slot in texture atlas, containing the pixel data of the glyph. - /// - API_FIELD() const FontTextureAtlasSlot* Slot; - - /// - /// The owner font. - /// - API_FIELD() const class Font* Font; -}; - -template<> -struct TIsPODType -{ - enum { Value = true }; -}; - /// /// Represents font object that can be using during text rendering (it uses Font Asset but with pre-cached data for chosen font properties). /// @@ -239,7 +136,6 @@ private: int32 _descender; int32 _lineGap; bool _hasKerning; - Dictionary _characters; mutable Dictionary _kerningTable; public: @@ -332,11 +228,6 @@ public: /// The text witch characters to cache. API_FUNCTION() void CacheText(const StringView& text); - /// - /// Invalidates all cached dynamic font atlases using this font. Can be used to reload font characters after changing font asset options. - /// - API_FUNCTION() void Invalidate(); - public: /// /// Processes text to get cached lines for rendering. diff --git a/Source/Engine/Render2D/FontAsset.cpp b/Source/Engine/Render2D/FontAsset.cpp index 34ffe7330..c16bd56b4 100644 --- a/Source/Engine/Render2D/FontAsset.cpp +++ b/Source/Engine/Render2D/FontAsset.cpp @@ -214,8 +214,10 @@ bool FontAsset::ContainsChar(Char c) const void FontAsset::Invalidate() { ScopeLock lock(Locker); - for (auto font : _fonts) - font->Invalidate(); + for (auto& entry : _characterCache) + FontManager::Invalidate(entry.Value); + + _characterCache.Clear(); } uint64 FontAsset::GetMemoryUsage() const diff --git a/Source/Engine/Render2D/FontAsset.h b/Source/Engine/Render2D/FontAsset.h index 4bf2114e1..fe4458f43 100644 --- a/Source/Engine/Render2D/FontAsset.h +++ b/Source/Engine/Render2D/FontAsset.h @@ -7,8 +7,113 @@ class Font; class FontManager; +struct FontTextureAtlasSlot; typedef struct FT_FaceRec_* FT_Face; +// Font glyph metrics: +// +// xmin xmax +// | | +// |<-------- width -------->| +// | | +// | +-------------------------+----------------- ymax +// | | ggggggggg ggggg | ^ ^ +// | | g:::::::::ggg::::g | | | +// | | g:::::::::::::::::g | | | +// | | g::::::ggggg::::::gg | | | +// | | g:::::g g:::::g | | | +// offsetX -|-------->| g:::::g g:::::g | offsetY | +// | | g:::::g g:::::g | | | +// | | g::::::g g:::::g | | | +// | | g:::::::ggggg:::::g | | | +// | | g::::::::::::::::g | | height +// | | gg::::::::::::::g | | | +// baseline ---*---------|---- gggggggg::::::g-----*-------- | +// / | | g:::::g | | +// origin | | gggggg g:::::g | | +// | | g:::::gg gg:::::g | | +// | | g::::::ggg:::::::g | | +// | | gg:::::::::::::g | | +// | | ggg::::::ggg | | +// | | gggggg | v +// | +-------------------------+----------------- ymin +// | | +// |------------- advanceX ----------->| + +/// +/// The cached font character entry (read for rendering and further processing). +/// +API_STRUCT(NoDefault) struct FLAXENGINE_API FontCharacterEntry +{ + DECLARE_SCRIPTING_TYPE_MINIMAL(FontCharacterEntry); + + /// + /// The character represented by this entry. + /// + API_FIELD() Char Character; + + /// + /// True if entry is valid, otherwise false. + /// + API_FIELD() bool IsValid = false; + + /// + /// The index to a specific texture in the font cache. + /// + API_FIELD() byte TextureIndex; + + /// + /// The left bearing expressed in integer pixels. + /// + API_FIELD() int16 OffsetX; + + /// + /// The top bearing expressed in integer pixels. + /// + API_FIELD() int16 OffsetY; + + /// + /// The amount to advance in X before drawing the next character in a string. + /// + API_FIELD() int16 AdvanceX; + + /// + /// The distance from baseline to glyph top most point. + /// + API_FIELD() int16 BearingY; + + /// + /// The height in pixels of the glyph. + /// + API_FIELD() int16 Height; + + /// + /// The start location of the character in the texture (in texture coordinates space). + /// + API_FIELD() Float2 UV; + + /// + /// The size the character in the texture (in texture coordinates space). + /// + API_FIELD() Float2 UVSize; + + /// + /// The slot in texture atlas, containing the pixel data of the glyph. + /// + API_FIELD() const FontTextureAtlasSlot* Slot; + + /// + /// The owner font. + /// + API_FIELD() const class Font* Font; +}; + +template<> +struct TIsPODType +{ + enum { Value = true }; +}; + /// /// The font hinting used when rendering characters. /// @@ -125,6 +230,7 @@ private: FontOptions _options; BytesContainer _fontFile; Array> _fonts; + Dictionary, FontCharacterEntry> _characterCache; AssetReference _virtualBold; AssetReference _virtualItalic; AssetReference _virtualMSDF; From cf05034d7a2e92160a12249563674f1fd980d321 Mon Sep 17 00:00:00 2001 From: fibref Date: Fri, 14 Aug 2026 21:02:03 +0800 Subject: [PATCH 04/10] fix Font and FontAsset cache not completely refreshed when options change --- Source/Editor/Windows/Assets/FontWindow.cs | 1 - Source/Engine/Render2D/Font.cpp | 18 ++++++++++++++-- Source/Engine/Render2D/Font.h | 5 +++++ Source/Engine/Render2D/FontAsset.cpp | 24 ++++++++++++++++++++++ 4 files changed, 45 insertions(+), 3 deletions(-) diff --git a/Source/Editor/Windows/Assets/FontWindow.cs b/Source/Editor/Windows/Assets/FontWindow.cs index 0f0bc1630..71f8d800d 100644 --- a/Source/Editor/Windows/Assets/FontWindow.cs +++ b/Source/Editor/Windows/Assets/FontWindow.cs @@ -141,7 +141,6 @@ namespace FlaxEditor.Windows.Assets if (assetOptions != options) { Asset.Options = options; - Asset.Invalidate(); } } diff --git a/Source/Engine/Render2D/Font.cpp b/Source/Engine/Render2D/Font.cpp index ed3093f35..10b4b116f 100644 --- a/Source/Engine/Render2D/Font.cpp +++ b/Source/Engine/Render2D/Font.cpp @@ -112,6 +112,21 @@ void Font::CacheText(const StringView& text) } } +void Font::Invalidate() +{ + ScopeLock lock(_asset->Locker); + + FlushFaceSize(); + const FT_Face face = _asset->GetFTFace(); + ASSERT(face != nullptr); + _height = Convert26Dot6ToRoundedPixel(FT_MulFix(face->height, face->size->metrics.y_scale)); + _hasKerning = FT_HAS_KERNING(face) != 0; + _ascender = Convert26Dot6ToRoundedPixel(face->size->metrics.ascender); + _descender = Convert26Dot6ToRoundedPixel(face->size->metrics.descender); + _lineGap = _height - _ascender + _descender; + _kerningTable.Clear(); +} + void Font::ProcessText(const StringView& text, Array>& outputLines, const TextLayoutOptions& layout) { int32 textLength = text.Length(); @@ -463,8 +478,7 @@ void Font::FlushFaceSize() const { // Set the character size const FT_Face face = _asset->GetFTFace(); - FontOptions options = _asset->GetOptions(); - float size = options.RasterMode == FontRasterMode::MSDF ? options.MSDFSize : _size; + float size = _asset->GetOptions().RasterMode == FontRasterMode::MSDF ? _asset->GetOptions().MSDFSize : _size; const FT_Error error = FT_Set_Char_Size(face, 0, ConvertPixelTo26Dot6(size * FontManager::FontScale), DefaultDPI, DefaultDPI); if (error) { diff --git a/Source/Engine/Render2D/Font.h b/Source/Engine/Render2D/Font.h index f176bb3f3..ace21e64a 100644 --- a/Source/Engine/Render2D/Font.h +++ b/Source/Engine/Render2D/Font.h @@ -228,6 +228,11 @@ public: /// The text witch characters to cache. API_FUNCTION() void CacheText(const StringView& text); + /// + /// Refresh cached metrics. Can be used after changing font asset options. + /// + API_FUNCTION() void Invalidate(); + public: /// /// Processes text to get cached lines for rendering. diff --git a/Source/Engine/Render2D/FontAsset.cpp b/Source/Engine/Render2D/FontAsset.cpp index c16bd56b4..39fe89207 100644 --- a/Source/Engine/Render2D/FontAsset.cpp +++ b/Source/Engine/Render2D/FontAsset.cpp @@ -101,6 +101,26 @@ FontFlags FontAsset::GetStyle() const void FontAsset::SetOptions(const FontOptions& value) { _options = value; + Invalidate(); + + if (_virtualBold) + { + auto options = _options; + options.Flags |= FontFlags::Bold; + _virtualBold->SetOptions(options); + } + if (_virtualItalic) + { + auto options = _options; + options.Flags |= FontFlags::Italic; + _virtualItalic->SetOptions(options); + } + if (_virtualMSDF) + { + auto options = _options; + options.RasterMode = FontRasterMode::MSDF; + _virtualMSDF->SetOptions(options); + } } Font* FontAsset::CreateFont(float size) @@ -218,6 +238,10 @@ void FontAsset::Invalidate() FontManager::Invalidate(entry.Value); _characterCache.Clear(); + + // Refresh cached metrics of all fonts created from this asset + for (auto font : _fonts) + font->Invalidate(); } uint64 FontAsset::GetMemoryUsage() const From 32d3f69d00b77cb66e7216950595c0b5816d2cfe Mon Sep 17 00:00:00 2001 From: fibref Date: Sat, 15 Aug 2026 18:11:40 +0800 Subject: [PATCH 05/10] fix fallback font not scaled correctly --- Source/Engine/Render2D/Font.cpp | 41 ++++++++++++++++++---------- Source/Engine/Render2D/Font.h | 7 +++++ Source/Engine/Render2D/FontAsset.cpp | 24 ++++++++-------- Source/Engine/Render2D/FontAsset.h | 6 ++-- Source/Engine/Render2D/Render2D.cpp | 28 +++++++++++-------- Source/Engine/UI/TextRender.cpp | 13 +++++---- 6 files changed, 72 insertions(+), 47 deletions(-) diff --git a/Source/Engine/Render2D/Font.cpp b/Source/Engine/Render2D/Font.cpp index 10b4b116f..fb88c8256 100644 --- a/Source/Engine/Render2D/Font.cpp +++ b/Source/Engine/Render2D/Font.cpp @@ -37,14 +37,23 @@ void Font::GetCharacter(Char c, FontCharacterEntry& result, bool enableFallback) { // Try to get the character or cache it if cannot be found const auto key = Pair(_asset->GetOptions().RasterMode == FontRasterMode::MSDF ? _asset->GetOptions().MSDFSize : GetSize(), c); - if (!_asset->_characterCache.TryGet(key, result)) + if (_asset->_characterCache.TryGet(key, result)) + { + // With MSDF font introduced, cached entry may be created by a different font (with same MSDFSize) + // This is to ensure returned entry has a reference to a font whose size matches the font being used to render + result.Font = this; + } + else { // This thread race condition may happen in editor but in game we usually do all stuff with fonts on main thread (chars caching) ScopeLock lock(_asset->Locker); // Handle situation when more than one thread wants to get the same character if (_asset->_characterCache.TryGet(key, result)) + { + result.Font = this; return; + } // Try to use fallback font if character is missing if (enableFallback && !_asset->ContainsChar(c)) @@ -52,13 +61,9 @@ void Font::GetCharacter(Char c, FontCharacterEntry& result, bool enableFallback) for (int32 fallbackIndex = 0; fallbackIndex < FallbackFonts.Count(); fallbackIndex++) { FontAsset* fallbackFont = FallbackFonts.Get()[fallbackIndex].Get(); - if (fallbackFont && _asset->GetOptions().RasterMode == FontRasterMode::MSDF) - { - fallbackFont = fallbackFont->GetMSDF(); - } if (fallbackFont && fallbackFont->ContainsChar(c)) { - fallbackFont->CreateFont(GetSize())->GetCharacter(c, result, enableFallback); + fallbackFont->GetRasterMode(_asset->GetOptions().RasterMode)->CreateFont(GetSize())->GetCharacter(c, result, enableFallback); return; } } @@ -127,6 +132,11 @@ void Font::Invalidate() _kerningTable.Clear(); } +float Font::GetScale(float layoutScale) const +{ + return layoutScale / FontManager::FontScale * (_asset->GetOptions().RasterMode == FontRasterMode::MSDF ? _size / _asset->GetOptions().MSDFSize : 1.0f); +} + void Font::ProcessText(const StringView& text, Array>& outputLines, const TextLayoutOptions& layout) { int32 textLength = text.Length(); @@ -137,7 +147,7 @@ void Font::ProcessText(const StringView& text, ArrayGetOptions().RasterMode == FontRasterMode::MSDF ? _size / _asset->GetOptions().MSDFSize : 1.0f); + const float scale = GetScale(layout.Scale); float boundsWidth = layout.Bounds.GetWidth(); float baseLinesDistance = static_cast(_height) * layout.BaseLinesGapScale * scale; tmpLine.Location = Float2::Zero; @@ -181,6 +191,7 @@ void Font::ProcessText(const StringView& text, ArrayGetScale(layout.Scale); // Get kerning if (!isWhitespace && previous.IsValid) @@ -192,7 +203,7 @@ void Font::ProcessText(const StringView& text, Array> lines; ProcessText(text, lines, layout); ASSERT(lines.HasItems()); - float scale = layout.Scale / FontManager::FontScale * (_asset->GetOptions().RasterMode == FontRasterMode::MSDF ? _size / _asset->GetOptions().MSDFSize : 1.0f); + const float scale = GetScale(layout.Scale); float baseLinesDistance = static_cast(_height) * layout.BaseLinesGapScale * scale; // Offset position to match lines origin space @@ -364,12 +375,13 @@ int32 Font::HitTestText(const StringView& text, const Float2& location, const Te // Cache current character const Char currentChar = text[currentIndex]; GetCharacter(currentChar, entry); + const float entryScale = entry.Font->GetScale(layout.Scale); const bool isWhitespace = StringUtils::IsWhitespace(currentChar); // Apply kerning if (!isWhitespace && previous.IsValid) { - x += entry.Font->GetKerning(previous.Character, entry.Character); + x += entry.Font->GetKerning(previous.Character, entry.Character) * entryScale; } previous = entry; @@ -388,7 +400,7 @@ int32 Font::HitTestText(const StringView& text, const Float2& location, const Te } // Move - x += entry.AdvanceX * scale; + x += entry.AdvanceX * entryScale; } // Test line end edge @@ -431,7 +443,7 @@ Float2 Font::GetCharPosition(const StringView& text, int32 index, const TextLayo Array> lines; ProcessText(text, lines, layout); ASSERT(lines.HasItems()); - float scale = layout.Scale / FontManager::FontScale * (_asset->GetOptions().RasterMode == FontRasterMode::MSDF ? _size / _asset->GetOptions().MSDFSize : 1.0f); + const float scale = GetScale(layout.Scale); float baseLinesDistance = static_cast(_height) * layout.BaseLinesGapScale * scale; // Find line with that position @@ -452,17 +464,18 @@ Float2 Font::GetCharPosition(const StringView& text, int32 index, const TextLayo // Cache current character const Char currentChar = text[currentIndex]; GetCharacter(currentChar, entry); + const float entryScale = entry.Font->GetScale(layout.Scale); const bool isWhitespace = StringUtils::IsWhitespace(currentChar); // Apply kerning if (!isWhitespace && previous.IsValid) { - charPos.X += entry.Font->GetKerning(previous.Character, entry.Character); + charPos.X += entry.Font->GetKerning(previous.Character, entry.Character) * entryScale; } previous = entry; // Move - charPos.X += entry.AdvanceX * scale; + charPos.X += entry.AdvanceX * entryScale; } // Upper left corner of the character diff --git a/Source/Engine/Render2D/Font.h b/Source/Engine/Render2D/Font.h index ace21e64a..c8fd960d0 100644 --- a/Source/Engine/Render2D/Font.h +++ b/Source/Engine/Render2D/Font.h @@ -234,6 +234,13 @@ public: API_FUNCTION() void Invalidate(); public: + /// + /// Gets the scale factor that maps the rasterized font size to the actual rendered size. + /// + /// The layout scale. + /// The scale factor. + float GetScale(float layoutScale) const; + /// /// Processes text to get cached lines for rendering. /// diff --git a/Source/Engine/Render2D/FontAsset.cpp b/Source/Engine/Render2D/FontAsset.cpp index 39fe89207..adc641f0d 100644 --- a/Source/Engine/Render2D/FontAsset.cpp +++ b/Source/Engine/Render2D/FontAsset.cpp @@ -69,7 +69,7 @@ void FontAsset::unload(bool isReloading) _fontFile.Release(); _virtualBold = nullptr; _virtualItalic = nullptr; - _virtualMSDF = nullptr; + _virtualRasterMode = nullptr; } AssetChunksFlag FontAsset::getChunksToPreload() const @@ -115,11 +115,11 @@ void FontAsset::SetOptions(const FontOptions& value) options.Flags |= FontFlags::Italic; _virtualItalic->SetOptions(options); } - if (_virtualMSDF) + if (_virtualRasterMode) { auto options = _options; - options.RasterMode = FontRasterMode::MSDF; - _virtualMSDF->SetOptions(options); + options.RasterMode = _options.RasterMode == FontRasterMode::MSDF ? FontRasterMode::Bitmap : FontRasterMode::MSDF; + _virtualRasterMode->SetOptions(options); } } @@ -176,20 +176,20 @@ FontAsset* FontAsset::GetItalic() return _virtualItalic; } -FontAsset* FontAsset::GetMSDF() +FontAsset* FontAsset::GetRasterMode(FontRasterMode rasterMode) { ScopeLock lock(Locker); - if (_options.RasterMode == FontRasterMode::MSDF) + if (_options.RasterMode == rasterMode) return this; - if (!_virtualMSDF) + if (!_virtualRasterMode) { - _virtualMSDF = Content::CreateVirtualAsset(); - _virtualMSDF->Init(_fontFile); + _virtualRasterMode = Content::CreateVirtualAsset(); + _virtualRasterMode->Init(_fontFile); auto options = _options; - options.RasterMode = FontRasterMode::MSDF; - _virtualMSDF->SetOptions(options); + options.RasterMode = rasterMode; + _virtualRasterMode->SetOptions(options); } - return _virtualMSDF; + return _virtualRasterMode; } bool FontAsset::Init(const BytesContainer& fontFile) diff --git a/Source/Engine/Render2D/FontAsset.h b/Source/Engine/Render2D/FontAsset.h index fe4458f43..0529469b9 100644 --- a/Source/Engine/Render2D/FontAsset.h +++ b/Source/Engine/Render2D/FontAsset.h @@ -233,7 +233,7 @@ private: Dictionary, FontCharacterEntry> _characterCache; AssetReference _virtualBold; AssetReference _virtualItalic; - AssetReference _virtualMSDF; + AssetReference _virtualRasterMode; public: /// @@ -293,10 +293,10 @@ public: API_FUNCTION() FontAsset* GetItalic(); /// - /// Gets the MSDF version of the font. Returns itself or creates a new virtual font asset using this font but rasterized with Multi-channel Signed Distance Field (MSDF). + /// Gets the different rasterization mode of the font. Returns itself or creates a new virtual font asset using this font but rasterized with the specified mode. /// /// The virtual font or this. - API_FUNCTION() FontAsset* GetMSDF(); + API_FUNCTION() FontAsset* GetRasterMode(FontRasterMode rasterMode); /// /// Initializes the font with a custom font file data. diff --git a/Source/Engine/Render2D/Render2D.cpp b/Source/Engine/Render2D/Render2D.cpp index efabc4b58..de6ad9215 100644 --- a/Source/Engine/Render2D/Render2D.cpp +++ b/Source/Engine/Render2D/Render2D.cpp @@ -1204,7 +1204,7 @@ void Render2D::DrawText(Font* font, const StringView& text, const Color& color, FontCharacterEntry previous; int32 kerning; FontOptions options = font->GetAsset()->GetOptions(); - float scale = 1.0f / FontManager::FontScale * (options.RasterMode == FontRasterMode::MSDF ? font->GetSize() / options.MSDFSize : 1.0f); + const float scale = font->GetScale(1.0f); const bool enableFallbackFonts = EnumHasAllFlags(Features, RenderingFeatures::FallbackFonts); // Render all characters @@ -1231,6 +1231,8 @@ void Render2D::DrawText(Font* font, const StringView& text, const Color& color, { // Get character entry font->GetCharacter(currentChar, entry, enableFallbackFonts); + // Fallback fonts may have different MSDFSize, so we need to calculate scale per character + const float entryScale = entry.Font->GetScale(1.0f); // Check if need to select/change font atlas (since characters even in the same font may be located in different atlases) if (fontAtlas == nullptr || entry.TextureIndex != fontAtlasIndex) @@ -1263,17 +1265,17 @@ void Render2D::DrawText(Font* font, const StringView& text, const Color& color, { kerning = 0; } - pointer.X += kerning * scale; + pointer.X += kerning * entryScale; previous = entry; // Omit whitespace characters if (!isWhitespace) { // Calculate character size and atlas coordinates - const float x = pointer.X + entry.OffsetX * scale; - const float y = pointer.Y + (font->GetHeight() + font->GetDescender() - entry.OffsetY) * scale; + const float x = pointer.X + entry.OffsetX * entryScale; + const float y = pointer.Y - entry.OffsetY * entryScale + (font->GetHeight() + font->GetDescender()) * scale; - Rectangle charRect(x, y, entry.UVSize.X * scale, entry.UVSize.Y * scale); + Rectangle charRect(x, y, entry.UVSize.X * entryScale, entry.UVSize.Y * entryScale); Float2 upperLeftUV = entry.UV * invAtlasSize; Float2 rightBottomUV = (entry.UV + entry.UVSize) * invAtlasSize; @@ -1286,7 +1288,7 @@ void Render2D::DrawText(Font* font, const StringView& text, const Color& color, } // Move - pointer.X += entry.AdvanceX * scale; + pointer.X += entry.AdvanceX * entryScale; } else { @@ -1320,7 +1322,7 @@ void Render2D::DrawText(Font* font, const StringView& text, const Color& color, FontCharacterEntry previous; int32 kerning; FontOptions options = font->GetAsset()->GetOptions(); - float scale = layout.Scale / FontManager::FontScale * (options.RasterMode == FontRasterMode::MSDF ? font->GetSize() / options.MSDFSize : 1.0f); + const float scale = font->GetScale(layout.Scale); const bool enableFallbackFonts = EnumHasAllFlags(Features, RenderingFeatures::FallbackFonts); // Process text to get lines @@ -1356,6 +1358,8 @@ void Render2D::DrawText(Font* font, const StringView& text, const Color& color, { // Get character entry font->GetCharacter(currentChar, entry, enableFallbackFonts); + // Fallback fonts may have different MSDFSize, so we need to calculate scale per character + const float entryScale = entry.Font->GetScale(layout.Scale); // Check if need to select/change font atlas (since characters even in the same font may be located in different atlases) if (fontAtlas == nullptr || entry.TextureIndex != fontAtlasIndex) @@ -1386,17 +1390,17 @@ void Render2D::DrawText(Font* font, const StringView& text, const Color& color, { kerning = 0; } - pointer.X += (float)kerning * scale; + pointer.X += (float)kerning * entryScale; previous = entry; // Omit whitespace characters if (!isWhitespace) { // Calculate character size and atlas coordinates - const float x = pointer.X + entry.OffsetX * scale; - const float y = pointer.Y - entry.OffsetY * scale + Math::Ceil((font->GetHeight() + font->GetDescender()) * scale); + const float x = pointer.X + entry.OffsetX * entryScale; + const float y = pointer.Y - entry.OffsetY * entryScale + Math::Ceil((font->GetHeight() + font->GetDescender()) * scale); - Rectangle charRect(x, y, entry.UVSize.X * scale, entry.UVSize.Y * scale); + Rectangle charRect(x, y, entry.UVSize.X * entryScale, entry.UVSize.Y * entryScale); charRect.Offset(layout.Bounds.Location); Float2 upperLeftUV = entry.UV * invAtlasSize; @@ -1410,7 +1414,7 @@ void Render2D::DrawText(Font* font, const StringView& text, const Color& color, } // Move - pointer.X += entry.AdvanceX * scale; + pointer.X += entry.AdvanceX * entryScale; } } } diff --git a/Source/Engine/UI/TextRender.cpp b/Source/Engine/UI/TextRender.cpp index 88e5f8b2b..ef95d27c4 100644 --- a/Source/Engine/UI/TextRender.cpp +++ b/Source/Engine/UI/TextRender.cpp @@ -172,7 +172,7 @@ void TextRender::UpdateLayout() // Pick a font (remove DPI text scale as the text is being placed in the world) auto font = Font->CreateFont(_size); - float scale = _layoutOptions.Scale / FontManager::FontScale * (Font->GetOptions().RasterMode == FontRasterMode::MSDF ? _size / Font->GetOptions().MSDFSize : 1.0f); + const float scale = font->GetScale(_layoutOptions.Scale); // Prepare FontTextureAtlas* fontAtlas = nullptr; @@ -217,6 +217,7 @@ void TextRender::UpdateLayout() if (c != '\n') { font->GetCharacter(c, entry); + const float entryScale = entry.Font->GetScale(_layoutOptions.Scale); // Check if need to select/change font atlas (since characters even in the same font may be located in different atlases) if (fontAtlas == nullptr || entry.TextureIndex != drawChunk.FontAtlasIndex) @@ -273,17 +274,17 @@ void TextRender::UpdateLayout() { kerning = 0; } - pointer.X += (float)kerning * scale; + pointer.X += (float)kerning * entryScale; previous = entry; // Omit whitespace characters if (!isWhitespace) { // Calculate character size and atlas coordinates - const float x = pointer.X + (float)entry.OffsetX * scale; - const float y = pointer.Y + (float)(font->GetHeight() + font->GetDescender() - entry.OffsetY) * scale; + const float x = pointer.X + (float)entry.OffsetX * entryScale; + const float y = pointer.Y - (float)entry.OffsetY * entryScale + (float)(font->GetHeight() + font->GetDescender()) * scale; - Rectangle charRect(x, y, entry.UVSize.X * scale, entry.UVSize.Y * scale); + Rectangle charRect(x, y, entry.UVSize.X * entryScale, entry.UVSize.Y * entryScale); charRect.Offset(_layoutOptions.Bounds.Location); Float2 upperLeftUV = entry.UV * invAtlasSize; @@ -326,7 +327,7 @@ void TextRender::UpdateLayout() } // Move - pointer.X += (float)entry.AdvanceX * scale; + pointer.X += (float)entry.AdvanceX * entryScale; } } } From c41c5ad561fa256418a4d0d153eb10808593b1d2 Mon Sep 17 00:00:00 2001 From: fibref Date: Sat, 15 Aug 2026 18:15:44 +0800 Subject: [PATCH 06/10] add MSDFSize slider limit --- Source/Editor/Windows/Assets/FontWindow.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Editor/Windows/Assets/FontWindow.cs b/Source/Editor/Windows/Assets/FontWindow.cs index 71f8d800d..fe05b3b53 100644 --- a/Source/Editor/Windows/Assets/FontWindow.cs +++ b/Source/Editor/Windows/Assets/FontWindow.cs @@ -27,7 +27,7 @@ namespace FlaxEditor.Windows.Assets public FontRasterMode RasterMode; [DefaultValue(32.0f)] - [EditorOrder(6), EditorDisplay("Properties"), Tooltip("The font size used when generating MSDF font atlases.")] + [EditorOrder(6), Limit(4, 512), EditorDisplay("Properties"), Tooltip("The font size used when generating MSDF font atlases.")] public float MSDFSize; [DefaultValue(FontHinting.Default)] From 8fef3b7f651e693887fa73b4e210b2308cc8ae98 Mon Sep 17 00:00:00 2001 From: fibref Date: Sat, 15 Aug 2026 19:22:41 +0800 Subject: [PATCH 07/10] fix missing include --- Source/Engine/Render2D/FontAsset.h | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Source/Engine/Render2D/FontAsset.h b/Source/Engine/Render2D/FontAsset.h index 0529469b9..c8bcf435c 100644 --- a/Source/Engine/Render2D/FontAsset.h +++ b/Source/Engine/Render2D/FontAsset.h @@ -4,6 +4,9 @@ #include "Engine/Content/BinaryAsset.h" #include "Engine/Content/AssetReference.h" +#include "Engine/Core/Collections/Array.h" +#include "Engine/Core/Collections/Dictionary.h" +#include "Engine/Core/Math/Vector2.h" class Font; class FontManager; From 451985612b22d0ec088ea2fc6a43704d48b4cda0 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Sat, 19 Sep 2026 20:05:36 +0200 Subject: [PATCH 08/10] Various tweaks to #4218 --- Source/Editor/Windows/Assets/FontWindow.cs | 5 +++-- Source/Engine/Render2D/Font.cpp | 22 ++++++++++++++++++---- Source/Engine/Render2D/Font.h | 4 ++-- Source/Engine/Render2D/FontAsset.cpp | 3 ++- Source/Engine/Render2D/Render2D.cpp | 10 ++++------ 5 files changed, 29 insertions(+), 15 deletions(-) diff --git a/Source/Editor/Windows/Assets/FontWindow.cs b/Source/Editor/Windows/Assets/FontWindow.cs index fe05b3b53..2f27a3f1b 100644 --- a/Source/Editor/Windows/Assets/FontWindow.cs +++ b/Source/Editor/Windows/Assets/FontWindow.cs @@ -1,6 +1,5 @@ // Copyright (c) Wojciech Figat. All rights reserved. -using System; using System.ComponentModel; using FlaxEditor.Content; using FlaxEditor.CustomEditors; @@ -26,7 +25,7 @@ namespace FlaxEditor.Windows.Assets [EditorOrder(5), EditorDisplay("Properties"), Tooltip("The rasterization mode used when generating font atlases.")] public FontRasterMode RasterMode; - [DefaultValue(32.0f)] + [DefaultValue(32.0f), VisibleIf(nameof(IsMSDF))] [EditorOrder(6), Limit(4, 512), EditorDisplay("Properties"), Tooltip("The font size used when generating MSDF font atlases.")] public float MSDFSize; @@ -46,6 +45,8 @@ namespace FlaxEditor.Windows.Assets [EditorOrder(40), EditorDisplay("Properties"), Tooltip("Enables slant effect, emulating italic style.")] public bool Italic; + private bool IsMSDF => RasterMode == FontRasterMode.MSDF; + public void Get(out FontOptions options) { options = new FontOptions diff --git a/Source/Engine/Render2D/Font.cpp b/Source/Engine/Render2D/Font.cpp index fb88c8256..37ce0f505 100644 --- a/Source/Engine/Render2D/Font.cpp +++ b/Source/Engine/Render2D/Font.cpp @@ -36,7 +36,8 @@ Font::~Font() void Font::GetCharacter(Char c, FontCharacterEntry& result, bool enableFallback) { // Try to get the character or cache it if cannot be found - const auto key = Pair(_asset->GetOptions().RasterMode == FontRasterMode::MSDF ? _asset->GetOptions().MSDFSize : GetSize(), c); + const FontOptions& options = _asset->GetOptions(); + const auto key = Pair(options.RasterMode == FontRasterMode::MSDF ? options.MSDFSize : GetSize(), c); if (_asset->_characterCache.TryGet(key, result)) { // With MSDF font introduced, cached entry may be created by a different font (with same MSDFSize) @@ -63,7 +64,7 @@ void Font::GetCharacter(Char c, FontCharacterEntry& result, bool enableFallback) FontAsset* fallbackFont = FallbackFonts.Get()[fallbackIndex].Get(); if (fallbackFont && fallbackFont->ContainsChar(c)) { - fallbackFont->GetRasterMode(_asset->GetOptions().RasterMode)->CreateFont(GetSize())->GetCharacter(c, result, enableFallback); + fallbackFont->GetRasterMode(options.RasterMode)->CreateFont(GetSize())->GetCharacter(c, result, enableFallback); return; } } @@ -121,6 +122,17 @@ void Font::Invalidate() { ScopeLock lock(_asset->Locker); + // Invalidate cached characters (from atlas) + for (auto i = _asset->_characterCache.Begin(); i.IsNotEnd(); ++i) + { + if (i->Value.Font == this) + { + FontManager::Invalidate(i->Value); + _asset->_characterCache.Remove(i); + } + } + + // Rebuild font metrics FlushFaceSize(); const FT_Face face = _asset->GetFTFace(); ASSERT(face != nullptr); @@ -134,7 +146,8 @@ void Font::Invalidate() float Font::GetScale(float layoutScale) const { - return layoutScale / FontManager::FontScale * (_asset->GetOptions().RasterMode == FontRasterMode::MSDF ? _size / _asset->GetOptions().MSDFSize : 1.0f); + const FontOptions& options = _asset->GetOptions(); + return layoutScale / FontManager::FontScale * (options.RasterMode == FontRasterMode::MSDF ? _size / options.MSDFSize : 1.0f); } void Font::ProcessText(const StringView& text, Array>& outputLines, const TextLayoutOptions& layout) @@ -491,7 +504,8 @@ void Font::FlushFaceSize() const { // Set the character size const FT_Face face = _asset->GetFTFace(); - float size = _asset->GetOptions().RasterMode == FontRasterMode::MSDF ? _asset->GetOptions().MSDFSize : _size; + const FontOptions& options = _asset->GetOptions(); + float size = options.RasterMode == FontRasterMode::MSDF ? options.MSDFSize : _size; const FT_Error error = FT_Set_Char_Size(face, 0, ConvertPixelTo26Dot6(size * FontManager::FontScale), DefaultDPI, DefaultDPI); if (error) { diff --git a/Source/Engine/Render2D/Font.h b/Source/Engine/Render2D/Font.h index c8fd960d0..ada57ca9d 100644 --- a/Source/Engine/Render2D/Font.h +++ b/Source/Engine/Render2D/Font.h @@ -211,7 +211,7 @@ public: /// /// The character. /// The output character entry. - /// True if fallback to secondary font when the primary font doesn't contains this character. + /// True if fallback to secondary font when the primary font doesn't contain this character. void GetCharacter(Char c, FontCharacterEntry& result, bool enableFallback = true); /// @@ -229,7 +229,7 @@ public: API_FUNCTION() void CacheText(const StringView& text); /// - /// Refresh cached metrics. Can be used after changing font asset options. + /// Refresh cached metrics and invalidates all cached characters (in atlases). Can be used after changing font asset options. /// API_FUNCTION() void Invalidate(); diff --git a/Source/Engine/Render2D/FontAsset.cpp b/Source/Engine/Render2D/FontAsset.cpp index adc641f0d..1b58d472c 100644 --- a/Source/Engine/Render2D/FontAsset.cpp +++ b/Source/Engine/Render2D/FontAsset.cpp @@ -234,9 +234,10 @@ bool FontAsset::ContainsChar(Char c) const void FontAsset::Invalidate() { ScopeLock lock(Locker); + + // Invalidate cached characters (from atlas) for (auto& entry : _characterCache) FontManager::Invalidate(entry.Value); - _characterCache.Clear(); // Refresh cached metrics of all fonts created from this asset diff --git a/Source/Engine/Render2D/Render2D.cpp b/Source/Engine/Render2D/Render2D.cpp index de6ad9215..359190665 100644 --- a/Source/Engine/Render2D/Render2D.cpp +++ b/Source/Engine/Render2D/Render2D.cpp @@ -1203,7 +1203,7 @@ void Render2D::DrawText(Font* font, const StringView& text, const Color& color, Float2 invAtlasSize = Float2::One; FontCharacterEntry previous; int32 kerning; - FontOptions options = font->GetAsset()->GetOptions(); + const FontOptions& options = font->GetAsset()->GetOptions(); const float scale = font->GetScale(1.0f); const bool enableFallbackFonts = EnumHasAllFlags(Features, RenderingFeatures::FallbackFonts); @@ -1231,6 +1231,7 @@ void Render2D::DrawText(Font* font, const StringView& text, const Color& color, { // Get character entry font->GetCharacter(currentChar, entry, enableFallbackFonts); + // Fallback fonts may have different MSDFSize, so we need to calculate scale per character const float entryScale = entry.Font->GetScale(1.0f); @@ -1274,9 +1275,7 @@ void Render2D::DrawText(Font* font, const StringView& text, const Color& color, // Calculate character size and atlas coordinates const float x = pointer.X + entry.OffsetX * entryScale; const float y = pointer.Y - entry.OffsetY * entryScale + (font->GetHeight() + font->GetDescender()) * scale; - Rectangle charRect(x, y, entry.UVSize.X * entryScale, entry.UVSize.Y * entryScale); - Float2 upperLeftUV = entry.UV * invAtlasSize; Float2 rightBottomUV = (entry.UV + entry.UVSize) * invAtlasSize; @@ -1321,7 +1320,7 @@ void Render2D::DrawText(Font* font, const StringView& text, const Color& color, Float2 invAtlasSize = Float2::One; FontCharacterEntry previous; int32 kerning; - FontOptions options = font->GetAsset()->GetOptions(); + const FontOptions& options = font->GetAsset()->GetOptions(); const float scale = font->GetScale(layout.Scale); const bool enableFallbackFonts = EnumHasAllFlags(Features, RenderingFeatures::FallbackFonts); @@ -1358,6 +1357,7 @@ void Render2D::DrawText(Font* font, const StringView& text, const Color& color, { // Get character entry font->GetCharacter(currentChar, entry, enableFallbackFonts); + // Fallback fonts may have different MSDFSize, so we need to calculate scale per character const float entryScale = entry.Font->GetScale(layout.Scale); @@ -1399,10 +1399,8 @@ void Render2D::DrawText(Font* font, const StringView& text, const Color& color, // Calculate character size and atlas coordinates const float x = pointer.X + entry.OffsetX * entryScale; const float y = pointer.Y - entry.OffsetY * entryScale + Math::Ceil((font->GetHeight() + font->GetDescender()) * scale); - Rectangle charRect(x, y, entry.UVSize.X * entryScale, entry.UVSize.Y * entryScale); charRect.Offset(layout.Bounds.Location); - Float2 upperLeftUV = entry.UV * invAtlasSize; Float2 rightBottomUV = (entry.UV + entry.UVSize) * invAtlasSize; From 8565e8bfd10a4e3b6d6488dfead0514e749e1d56 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Sat, 19 Sep 2026 20:05:42 +0200 Subject: [PATCH 09/10] Update Editor assets --- Content/Editor/Fonts/Inconsolata-Regular.flax | 4 ++-- Content/Editor/Fonts/NotoSansSC-Regular.flax | 4 ++-- Content/Editor/Fonts/Roboto-Regular.flax | 4 ++-- Content/Editor/Fonts/SegMDL2.flax | 4 ++-- Content/Editor/Fonts/Segoe Media Center Regular.flax | 4 ++-- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/Content/Editor/Fonts/Inconsolata-Regular.flax b/Content/Editor/Fonts/Inconsolata-Regular.flax index d85ef610a..6046f2793 100644 --- a/Content/Editor/Fonts/Inconsolata-Regular.flax +++ b/Content/Editor/Fonts/Inconsolata-Regular.flax @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:139e2f54a268f4febc753aaa9807a33a6eb2dd84e788c5b8b4f50ca8683d2b3b -size 93116 +oid sha256:e931314736f3fd3aaa7e22a83af1fc8512824e6d6c83cde9edfc21d69d83ad23 +size 93121 diff --git a/Content/Editor/Fonts/NotoSansSC-Regular.flax b/Content/Editor/Fonts/NotoSansSC-Regular.flax index 8f8f00157..c458b3255 100644 --- a/Content/Editor/Fonts/NotoSansSC-Regular.flax +++ b/Content/Editor/Fonts/NotoSansSC-Regular.flax @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2ddc7af5fab1dd4ad858ad2abf11c6c243d908345720892cd56242fbc9c33b02 -size 10560900 +oid sha256:b19ad5b09e6fe82f7547e13d8b5dc63b84fb8253654a1f98ef784109b3d33e80 +size 10560905 diff --git a/Content/Editor/Fonts/Roboto-Regular.flax b/Content/Editor/Fonts/Roboto-Regular.flax index 01251cac6..fca17395b 100644 --- a/Content/Editor/Fonts/Roboto-Regular.flax +++ b/Content/Editor/Fonts/Roboto-Regular.flax @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c0584b3a3c186364464de155edece5cb6a8c46000676ac82a45f951f3a67fac5 -size 172194 +oid sha256:b96520c348ef1b63e7514dd739ce7440503e8b4462edd830a271b20dd60730c2 +size 172199 diff --git a/Content/Editor/Fonts/SegMDL2.flax b/Content/Editor/Fonts/SegMDL2.flax index b627849d9..ea9e3c4e2 100644 --- a/Content/Editor/Fonts/SegMDL2.flax +++ b/Content/Editor/Fonts/SegMDL2.flax @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f7a9d723d6342b8a484dfb2ee784cef506b3311a6092be0e9de35f941d698ff5 -size 204150 +oid sha256:eeeb5ea36b751130cce7b03127f7d1c26b05e906252c3532ed83a3e154a82bf9 +size 204155 diff --git a/Content/Editor/Fonts/Segoe Media Center Regular.flax b/Content/Editor/Fonts/Segoe Media Center Regular.flax index 6c17d96bf..06efc4cf0 100644 --- a/Content/Editor/Fonts/Segoe Media Center Regular.flax +++ b/Content/Editor/Fonts/Segoe Media Center Regular.flax @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c21443a111daede658cb634f56b4ae0838dbb2eaa75d4ba8ba74a4c77df44de8 -size 72798 +oid sha256:0c203e22557bbdb86f6b4c13f9fc954d88fff92911cc90706508be57506a5dd6 +size 72803 From e048913bfdfe96afde7bae2368846d10ed3a1f86 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Sat, 19 Sep 2026 20:15:02 +0200 Subject: [PATCH 10/10] Move `FontCharacterEntry` to a separate header file #4218 --- Source/Engine/Render2D/Font.h | 1 - Source/Engine/Render2D/FontAsset.h | 106 +------------------ Source/Engine/Render2D/FontCharacterEntry.h | 109 ++++++++++++++++++++ 3 files changed, 110 insertions(+), 106 deletions(-) create mode 100644 Source/Engine/Render2D/FontCharacterEntry.h diff --git a/Source/Engine/Render2D/Font.h b/Source/Engine/Render2D/Font.h index ada57ca9d..b3ddd25f8 100644 --- a/Source/Engine/Render2D/Font.h +++ b/Source/Engine/Render2D/Font.h @@ -10,7 +10,6 @@ #include "TextLayoutOptions.h" class FontAsset; -struct FontTextureAtlasSlot; struct FontCharacterEntry; // The default DPI that engine is using diff --git a/Source/Engine/Render2D/FontAsset.h b/Source/Engine/Render2D/FontAsset.h index c8bcf435c..0a5c67255 100644 --- a/Source/Engine/Render2D/FontAsset.h +++ b/Source/Engine/Render2D/FontAsset.h @@ -7,116 +7,12 @@ #include "Engine/Core/Collections/Array.h" #include "Engine/Core/Collections/Dictionary.h" #include "Engine/Core/Math/Vector2.h" +#include "Engine/Render2D/FontCharacterEntry.h" class Font; class FontManager; -struct FontTextureAtlasSlot; typedef struct FT_FaceRec_* FT_Face; -// Font glyph metrics: -// -// xmin xmax -// | | -// |<-------- width -------->| -// | | -// | +-------------------------+----------------- ymax -// | | ggggggggg ggggg | ^ ^ -// | | g:::::::::ggg::::g | | | -// | | g:::::::::::::::::g | | | -// | | g::::::ggggg::::::gg | | | -// | | g:::::g g:::::g | | | -// offsetX -|-------->| g:::::g g:::::g | offsetY | -// | | g:::::g g:::::g | | | -// | | g::::::g g:::::g | | | -// | | g:::::::ggggg:::::g | | | -// | | g::::::::::::::::g | | height -// | | gg::::::::::::::g | | | -// baseline ---*---------|---- gggggggg::::::g-----*-------- | -// / | | g:::::g | | -// origin | | gggggg g:::::g | | -// | | g:::::gg gg:::::g | | -// | | g::::::ggg:::::::g | | -// | | gg:::::::::::::g | | -// | | ggg::::::ggg | | -// | | gggggg | v -// | +-------------------------+----------------- ymin -// | | -// |------------- advanceX ----------->| - -/// -/// The cached font character entry (read for rendering and further processing). -/// -API_STRUCT(NoDefault) struct FLAXENGINE_API FontCharacterEntry -{ - DECLARE_SCRIPTING_TYPE_MINIMAL(FontCharacterEntry); - - /// - /// The character represented by this entry. - /// - API_FIELD() Char Character; - - /// - /// True if entry is valid, otherwise false. - /// - API_FIELD() bool IsValid = false; - - /// - /// The index to a specific texture in the font cache. - /// - API_FIELD() byte TextureIndex; - - /// - /// The left bearing expressed in integer pixels. - /// - API_FIELD() int16 OffsetX; - - /// - /// The top bearing expressed in integer pixels. - /// - API_FIELD() int16 OffsetY; - - /// - /// The amount to advance in X before drawing the next character in a string. - /// - API_FIELD() int16 AdvanceX; - - /// - /// The distance from baseline to glyph top most point. - /// - API_FIELD() int16 BearingY; - - /// - /// The height in pixels of the glyph. - /// - API_FIELD() int16 Height; - - /// - /// The start location of the character in the texture (in texture coordinates space). - /// - API_FIELD() Float2 UV; - - /// - /// The size the character in the texture (in texture coordinates space). - /// - API_FIELD() Float2 UVSize; - - /// - /// The slot in texture atlas, containing the pixel data of the glyph. - /// - API_FIELD() const FontTextureAtlasSlot* Slot; - - /// - /// The owner font. - /// - API_FIELD() const class Font* Font; -}; - -template<> -struct TIsPODType -{ - enum { Value = true }; -}; - /// /// The font hinting used when rendering characters. /// diff --git a/Source/Engine/Render2D/FontCharacterEntry.h b/Source/Engine/Render2D/FontCharacterEntry.h new file mode 100644 index 000000000..f73099beb --- /dev/null +++ b/Source/Engine/Render2D/FontCharacterEntry.h @@ -0,0 +1,109 @@ +// Copyright (c) Wojciech Figat. All rights reserved. + +#pragma once + +#include "Engine/Core/Math/Vector2.h" + +// Font glyph metrics: +// +// xmin xmax +// | | +// |<-------- width -------->| +// | | +// | +-------------------------+----------------- ymax +// | | ggggggggg ggggg | ^ ^ +// | | g:::::::::ggg::::g | | | +// | | g:::::::::::::::::g | | | +// | | g::::::ggggg::::::gg | | | +// | | g:::::g g:::::g | | | +// offsetX -|-------->| g:::::g g:::::g | offsetY | +// | | g:::::g g:::::g | | | +// | | g::::::g g:::::g | | | +// | | g:::::::ggggg:::::g | | | +// | | g::::::::::::::::g | | height +// | | gg::::::::::::::g | | | +// baseline ---*---------|---- gggggggg::::::g-----*-------- | +// / | | g:::::g | | +// origin | | gggggg g:::::g | | +// | | g:::::gg gg:::::g | | +// | | g::::::ggg:::::::g | | +// | | gg:::::::::::::g | | +// | | ggg::::::ggg | | +// | | gggggg | v +// | +-------------------------+----------------- ymin +// | | +// |------------- advanceX ----------->| + +/// +/// The cached font character entry (read for rendering and further processing). +/// +API_STRUCT(NoDefault) struct FLAXENGINE_API FontCharacterEntry +{ + DECLARE_SCRIPTING_TYPE_MINIMAL(FontCharacterEntry); + + /// + /// The character represented by this entry. + /// + API_FIELD() Char Character; + + /// + /// True if entry is valid, otherwise false. + /// + API_FIELD() bool IsValid = false; + + /// + /// The index to a specific texture in the font cache. + /// + API_FIELD() byte TextureIndex; + + /// + /// The left bearing expressed in integer pixels. + /// + API_FIELD() int16 OffsetX; + + /// + /// The top bearing expressed in integer pixels. + /// + API_FIELD() int16 OffsetY; + + /// + /// The amount to advance in X before drawing the next character in a string. + /// + API_FIELD() int16 AdvanceX; + + /// + /// The distance from baseline to glyph top most point. + /// + API_FIELD() int16 BearingY; + + /// + /// The height in pixels of the glyph. + /// + API_FIELD() int16 Height; + + /// + /// The start location of the character in the texture (in texture coordinates space). + /// + API_FIELD() Float2 UV; + + /// + /// The size the character in the texture (in texture coordinates space). + /// + API_FIELD() Float2 UVSize; + + /// + /// The slot in texture atlas, containing the pixel data of the glyph. + /// + API_FIELD() const struct FontTextureAtlasSlot* Slot; + + /// + /// The owner font. + /// + API_FIELD() const class Font* Font; +}; + +template<> +struct TIsPODType +{ + enum { Value = true }; +};