diff --git a/Dalamud/Configuration/Internal/DalamudConfiguration.cs b/Dalamud/Configuration/Internal/DalamudConfiguration.cs index 76c8f3603..66c2745c5 100644 --- a/Dalamud/Configuration/Internal/DalamudConfiguration.cs +++ b/Dalamud/Configuration/Internal/DalamudConfiguration.cs @@ -148,12 +148,9 @@ internal sealed class DalamudConfiguration : IServiceType, IDisposable public bool UseAxisFontsFromGame { get; set; } = false; /// - /// Gets or sets the gamma value to apply for Dalamud fonts. Effects text thickness. - /// - /// Before gamma is applied... - /// * ...TTF fonts loaded with stb or FreeType are in linear space. - /// * ...the game's prebaked AXIS fonts are in gamma space with gamma value of 1.4. + /// Gets or sets the gamma value to apply for Dalamud fonts. Do not use. /// + [Obsolete("It happens that nobody touched this setting", true)] public float FontGammaLevel { get; set; } = 1.4f; /// diff --git a/Dalamud/Interface/GameFonts/FdtFileView.cs b/Dalamud/Interface/GameFonts/FdtFileView.cs new file mode 100644 index 000000000..896a6dbb4 --- /dev/null +++ b/Dalamud/Interface/GameFonts/FdtFileView.cs @@ -0,0 +1,159 @@ +using System.Collections.Generic; +using System.IO; + +namespace Dalamud.Interface.GameFonts; + +/// +/// Reference member view of a .fdt file data. +/// +internal readonly unsafe struct FdtFileView +{ + private readonly byte* ptr; + + /// + /// Initializes a new instance of the struct. + /// + /// Pointer to the data. + /// Length of the data. + public FdtFileView(void* ptr, int length) + { + this.ptr = (byte*)ptr; + if (length < sizeof(FdtReader.FdtHeader)) + throw new InvalidDataException("Not enough space for a FdtHeader"); + + if (length < this.FileHeader.FontTableHeaderOffset + sizeof(FdtReader.FontTableHeader)) + throw new InvalidDataException("Not enough space for a FontTableHeader"); + if (length < this.FileHeader.FontTableHeaderOffset + sizeof(FdtReader.FontTableHeader) + + (sizeof(FdtReader.FontTableEntry) * this.FontHeader.FontTableEntryCount)) + throw new InvalidDataException("Not enough space for all the FontTableEntry"); + + if (length < this.FileHeader.KerningTableHeaderOffset + sizeof(FdtReader.KerningTableHeader)) + throw new InvalidDataException("Not enough space for a KerningTableHeader"); + if (length < this.FileHeader.KerningTableHeaderOffset + sizeof(FdtReader.KerningTableHeader) + + (sizeof(FdtReader.KerningTableEntry) * this.KerningEntryCount)) + throw new InvalidDataException("Not enough space for all the KerningTableEntry"); + } + + /// + /// Gets the file header. + /// + public ref FdtReader.FdtHeader FileHeader => ref *(FdtReader.FdtHeader*)this.ptr; + + /// + /// Gets the font header. + /// + public ref FdtReader.FontTableHeader FontHeader => + ref *(FdtReader.FontTableHeader*)((nint)this.ptr + this.FileHeader.FontTableHeaderOffset); + + /// + /// Gets the glyphs. + /// + public Span Glyphs => new(this.GlyphsUnsafe, this.FontHeader.FontTableEntryCount); + + /// + /// Gets the kerning header. + /// + public ref FdtReader.KerningTableHeader KerningHeader => + ref *(FdtReader.KerningTableHeader*)((nint)this.ptr + this.FileHeader.KerningTableHeaderOffset); + + /// + /// Gets the number of kerning entries. + /// + public int KerningEntryCount => Math.Min(this.FontHeader.KerningTableEntryCount, this.KerningHeader.Count); + + /// + /// Gets the kerning entries. + /// + public Span PairAdjustments => new( + this.ptr + this.FileHeader.KerningTableHeaderOffset + sizeof(FdtReader.KerningTableHeader), + this.KerningEntryCount); + + /// + /// Gets the maximum texture index. + /// + public int MaxTextureIndex + { + get + { + var i = 0; + foreach (ref var g in this.Glyphs) + { + if (g.TextureIndex > i) + i = g.TextureIndex; + } + + return i; + } + } + + private FdtReader.FontTableEntry* GlyphsUnsafe => + (FdtReader.FontTableEntry*)(this.ptr + this.FileHeader.FontTableHeaderOffset + + sizeof(FdtReader.FontTableHeader)); + + /// + /// Finds the glyph index for the corresponding codepoint. + /// + /// Unicode codepoint (UTF-32 value). + /// Corresponding index, or a negative number according to . + public int FindGlyphIndex(int codepoint) + { + var comp = FdtReader.CodePointToUtf8Int32(codepoint); + + var glyphs = this.GlyphsUnsafe; + var lo = 0; + var hi = this.FontHeader.FontTableEntryCount - 1; + while (lo <= hi) + { + var i = (int)(((uint)hi + (uint)lo) >> 1); + switch (comp.CompareTo(glyphs[i].CharUtf8)) + { + case 0: + return i; + case > 0: + lo = i + 1; + break; + default: + hi = i - 1; + break; + } + } + + return ~lo; + } + + /// + /// Create a glyph range for use with . + /// + /// Merge two ranges into one if distance is below the value specified in this parameter. + /// Glyph ranges. + public ushort[] ToGlyphRanges(int mergeDistance = 8) + { + var glyphs = this.Glyphs; + var ranges = new List(glyphs.Length) + { + checked((ushort)glyphs[0].CharInt), + checked((ushort)glyphs[0].CharInt), + }; + + foreach (ref var glyph in glyphs[1..]) + { + var c32 = glyph.CharInt; + if (c32 >= 0x10000) + break; + + var c16 = unchecked((ushort)c32); + if (ranges[^1] + mergeDistance >= c16 && c16 > ranges[^1]) + { + ranges[^1] = c16; + } + else if (ranges[^1] + 1 < c16) + { + ranges.Add(c16); + ranges.Add(c16); + } + } + + ranges.Add(0); + return ranges.ToArray(); + } +} diff --git a/Dalamud/Interface/GameFonts/GameFontFamilyAndSize.cs b/Dalamud/Interface/GameFonts/GameFontFamilyAndSize.cs index dd78baf87..6e66cf19b 100644 --- a/Dalamud/Interface/GameFonts/GameFontFamilyAndSize.cs +++ b/Dalamud/Interface/GameFonts/GameFontFamilyAndSize.cs @@ -3,7 +3,7 @@ namespace Dalamud.Interface.GameFonts; /// /// Enum of available game fonts in specific sizes. /// -public enum GameFontFamilyAndSize : int +public enum GameFontFamilyAndSize { /// /// Placeholder meaning unused. @@ -15,6 +15,7 @@ public enum GameFontFamilyAndSize : int /// /// Contains Japanese characters in addition to Latin characters. Used in game for the whole UI. /// + [GameFontFamilyAndSize("common/font/AXIS_96.fdt", "common/font/font{0}.tex", -1)] Axis96, /// @@ -22,6 +23,7 @@ public enum GameFontFamilyAndSize : int /// /// Contains Japanese characters in addition to Latin characters. Used in game for the whole UI. /// + [GameFontFamilyAndSize("common/font/AXIS_12.fdt", "common/font/font{0}.tex", -1)] Axis12, /// @@ -29,6 +31,7 @@ public enum GameFontFamilyAndSize : int /// /// Contains Japanese characters in addition to Latin characters. Used in game for the whole UI. /// + [GameFontFamilyAndSize("common/font/AXIS_14.fdt", "common/font/font{0}.tex", -1)] Axis14, /// @@ -36,6 +39,7 @@ public enum GameFontFamilyAndSize : int /// /// Contains Japanese characters in addition to Latin characters. Used in game for the whole UI. /// + [GameFontFamilyAndSize("common/font/AXIS_18.fdt", "common/font/font{0}.tex", -1)] Axis18, /// @@ -43,6 +47,7 @@ public enum GameFontFamilyAndSize : int /// /// Contains Japanese characters in addition to Latin characters. Used in game for the whole UI. /// + [GameFontFamilyAndSize("common/font/AXIS_36.fdt", "common/font/font{0}.tex", -4)] Axis36, /// @@ -50,6 +55,7 @@ public enum GameFontFamilyAndSize : int /// /// Serif font. Contains mostly ASCII range. Used in game for job names. /// + [GameFontFamilyAndSize("common/font/Jupiter_16.fdt", "common/font/font{0}.tex", -1)] Jupiter16, /// @@ -57,6 +63,7 @@ public enum GameFontFamilyAndSize : int /// /// Serif font. Contains mostly ASCII range. Used in game for job names. /// + [GameFontFamilyAndSize("common/font/Jupiter_20.fdt", "common/font/font{0}.tex", -1)] Jupiter20, /// @@ -64,6 +71,7 @@ public enum GameFontFamilyAndSize : int /// /// Serif font. Contains mostly ASCII range. Used in game for job names. /// + [GameFontFamilyAndSize("common/font/Jupiter_23.fdt", "common/font/font{0}.tex", -1)] Jupiter23, /// @@ -71,6 +79,7 @@ public enum GameFontFamilyAndSize : int /// /// Serif font. Contains mostly numbers. Used in game for flying texts. /// + [GameFontFamilyAndSize("common/font/Jupiter_45.fdt", "common/font/font{0}.tex", -2)] Jupiter45, /// @@ -78,6 +87,7 @@ public enum GameFontFamilyAndSize : int /// /// Serif font. Contains mostly ASCII range. Used in game for job names. /// + [GameFontFamilyAndSize("common/font/Jupiter_46.fdt", "common/font/font{0}.tex", -2)] Jupiter46, /// @@ -85,6 +95,7 @@ public enum GameFontFamilyAndSize : int /// /// Serif font. Contains mostly numbers. Used in game for flying texts. /// + [GameFontFamilyAndSize("common/font/Jupiter_90.fdt", "common/font/font{0}.tex", -4)] Jupiter90, /// @@ -92,6 +103,7 @@ public enum GameFontFamilyAndSize : int /// /// Horizontally wide. Contains mostly numbers. Used in game for HP/MP/IL stuff. /// + [GameFontFamilyAndSize("common/font/Meidinger_16.fdt", "common/font/font{0}.tex", -1)] Meidinger16, /// @@ -99,6 +111,7 @@ public enum GameFontFamilyAndSize : int /// /// Horizontally wide. Contains mostly numbers. Used in game for HP/MP/IL stuff. /// + [GameFontFamilyAndSize("common/font/Meidinger_20.fdt", "common/font/font{0}.tex", -1)] Meidinger20, /// @@ -106,6 +119,7 @@ public enum GameFontFamilyAndSize : int /// /// Horizontally wide. Contains mostly numbers. Used in game for HP/MP/IL stuff. /// + [GameFontFamilyAndSize("common/font/Meidinger_40.fdt", "common/font/font{0}.tex", -4)] Meidinger40, /// @@ -113,6 +127,7 @@ public enum GameFontFamilyAndSize : int /// /// Horizontally wide. Contains mostly ASCII range. /// + [GameFontFamilyAndSize("common/font/MiedingerMid_10.fdt", "common/font/font{0}.tex", -1)] MiedingerMid10, /// @@ -120,6 +135,7 @@ public enum GameFontFamilyAndSize : int /// /// Horizontally wide. Contains mostly ASCII range. /// + [GameFontFamilyAndSize("common/font/MiedingerMid_12.fdt", "common/font/font{0}.tex", -1)] MiedingerMid12, /// @@ -127,6 +143,7 @@ public enum GameFontFamilyAndSize : int /// /// Horizontally wide. Contains mostly ASCII range. /// + [GameFontFamilyAndSize("common/font/MiedingerMid_14.fdt", "common/font/font{0}.tex", -1)] MiedingerMid14, /// @@ -134,6 +151,7 @@ public enum GameFontFamilyAndSize : int /// /// Horizontally wide. Contains mostly ASCII range. /// + [GameFontFamilyAndSize("common/font/MiedingerMid_18.fdt", "common/font/font{0}.tex", -1)] MiedingerMid18, /// @@ -141,6 +159,7 @@ public enum GameFontFamilyAndSize : int /// /// Horizontally wide. Contains mostly ASCII range. /// + [GameFontFamilyAndSize("common/font/MiedingerMid_36.fdt", "common/font/font{0}.tex", -2)] MiedingerMid36, /// @@ -148,6 +167,7 @@ public enum GameFontFamilyAndSize : int /// /// Horizontally narrow. Contains mostly ASCII range. Used for addon titles. /// + [GameFontFamilyAndSize("common/font/TrumpGothic_184.fdt", "common/font/font{0}.tex", -1)] TrumpGothic184, /// @@ -155,6 +175,7 @@ public enum GameFontFamilyAndSize : int /// /// Horizontally narrow. Contains mostly ASCII range. Used for addon titles. /// + [GameFontFamilyAndSize("common/font/TrumpGothic_23.fdt", "common/font/font{0}.tex", -1)] TrumpGothic23, /// @@ -162,6 +183,7 @@ public enum GameFontFamilyAndSize : int /// /// Horizontally narrow. Contains mostly ASCII range. Used for addon titles. /// + [GameFontFamilyAndSize("common/font/TrumpGothic_34.fdt", "common/font/font{0}.tex", -1)] TrumpGothic34, /// @@ -169,5 +191,6 @@ public enum GameFontFamilyAndSize : int /// /// Horizontally narrow. Contains mostly ASCII range. Used for addon titles. /// + [GameFontFamilyAndSize("common/font/TrumpGothic_68.fdt", "common/font/font{0}.tex", -3)] TrumpGothic68, } diff --git a/Dalamud/Interface/GameFonts/GameFontFamilyAndSizeAttribute.cs b/Dalamud/Interface/GameFonts/GameFontFamilyAndSizeAttribute.cs new file mode 100644 index 000000000..f5260e4bc --- /dev/null +++ b/Dalamud/Interface/GameFonts/GameFontFamilyAndSizeAttribute.cs @@ -0,0 +1,37 @@ +namespace Dalamud.Interface.GameFonts; + +/// +/// Marks the path for an enum value. +/// +[AttributeUsage(AttributeTargets.Field)] +internal class GameFontFamilyAndSizeAttribute : Attribute +{ + /// + /// Initializes a new instance of the class. + /// + /// Inner path of the file. + /// the file path format for the relevant .tex files. + /// Horizontal offset of the corresponding font. + public GameFontFamilyAndSizeAttribute(string path, string texPathFormat, int horizontalOffset) + { + this.Path = path; + this.TexPathFormat = texPathFormat; + this.HorizontalOffset = horizontalOffset; + } + + /// + /// Gets the path. + /// + public string Path { get; } + + /// + /// Gets the file path format for the relevant .tex files.
+ /// Used for (, ). + ///
+ public string TexPathFormat { get; } + + /// + /// Gets the horizontal offset of the corresponding font. + /// + public int HorizontalOffset { get; } +} diff --git a/Dalamud/Interface/GameFonts/GameFontHandle.cs b/Dalamud/Interface/GameFonts/GameFontHandle.cs index d71e725c5..77461aa0a 100644 --- a/Dalamud/Interface/GameFonts/GameFontHandle.cs +++ b/Dalamud/Interface/GameFonts/GameFontHandle.cs @@ -1,75 +1,76 @@ -using System; using System.Numerics; +using Dalamud.Interface.ManagedFontAtlas; +using Dalamud.Interface.ManagedFontAtlas.Internals; + using ImGuiNET; namespace Dalamud.Interface.GameFonts; /// -/// Prepare and keep game font loaded for use in OnDraw. +/// ABI-compatible wrapper for . /// -public class GameFontHandle : IDisposable +public sealed class GameFontHandle : IFontHandle { - private readonly GameFontManager manager; - private readonly GameFontStyle fontStyle; + private readonly IFontHandle.IInternal fontHandle; + private readonly FontAtlasFactory fontAtlasFactory; /// /// Initializes a new instance of the class. /// - /// GameFontManager instance. - /// Font to use. - internal GameFontHandle(GameFontManager manager, GameFontStyle font) + /// The wrapped . + /// An instance of . + internal GameFontHandle(IFontHandle.IInternal fontHandle, FontAtlasFactory fontAtlasFactory) { - this.manager = manager; - this.fontStyle = font; + this.fontHandle = fontHandle; + this.fontAtlasFactory = fontAtlasFactory; } - /// - /// Gets the font style. - /// - public GameFontStyle Style => this.fontStyle; + /// + public Exception? LoadException => this.fontHandle.LoadException; + + /// + public bool Available => this.fontHandle.Available; + + /// + [Obsolete($"Use {nameof(Push)}, and then use {nameof(ImGui.GetFont)} instead.", false)] + public ImFontPtr ImFont => this.fontHandle.ImFont; /// - /// Gets a value indicating whether this font is ready for use. + /// Gets the font style. Only applicable for . /// - public bool Available - { - get - { - unsafe - { - return this.manager.GetFont(this.fontStyle).GetValueOrDefault(null).NativePtr != null; - } - } - } + [Obsolete("If you use this, let the fact that you use this be known at Dalamud Discord.", false)] + public GameFontStyle Style => ((GamePrebakedFontHandle)this.fontHandle).FontStyle; /// - /// Gets the font. + /// Gets the relevant .
+ ///
+ /// Only applicable for game fonts. Otherwise it will throw. ///
- public ImFontPtr ImFont => this.manager.GetFont(this.fontStyle).Value; + [Obsolete("If you use this, let the fact that you use this be known at Dalamud Discord.", false)] + public FdtReader FdtReader => this.fontAtlasFactory.GetFdtReader(this.Style.FamilyAndSize)!; + + /// + public void Dispose() => this.fontHandle.Dispose(); + + /// + public IDisposable Push() => this.fontHandle.Push(); /// - /// Gets the FdtReader. - /// - public FdtReader FdtReader => this.manager.GetFdtReader(this.fontStyle.FamilyAndSize); - - /// - /// Creates a new GameFontLayoutPlan.Builder. + /// Creates a new .
+ ///
+ /// Only applicable for game fonts. Otherwise it will throw. ///
/// Text. /// A new builder for GameFontLayoutPlan. - public GameFontLayoutPlan.Builder LayoutBuilder(string text) - { - return new GameFontLayoutPlan.Builder(this.ImFont, this.FdtReader, text); - } - - /// - public void Dispose() => this.manager.DecreaseFontRef(this.fontStyle); + [Obsolete("If you use this, let the fact that you use this be known at Dalamud Discord.", false)] + public GameFontLayoutPlan.Builder LayoutBuilder(string text) => new(this.ImFont, this.FdtReader, text); /// /// Draws text. /// /// Text to draw. + [Obsolete("If you use this, let the fact that you use this be known at Dalamud Discord.", false)] public void Text(string text) { if (!this.Available) @@ -93,6 +94,7 @@ public class GameFontHandle : IDisposable ///
/// Color. /// Text to draw. + [Obsolete("If you use this, let the fact that you use this be known at Dalamud Discord.", false)] public void TextColored(Vector4 col, string text) { ImGui.PushStyleColor(ImGuiCol.Text, col); @@ -104,6 +106,7 @@ public class GameFontHandle : IDisposable /// Draws disabled text. /// /// Text to draw. + [Obsolete("If you use this, let the fact that you use this be known at Dalamud Discord.", false)] public void TextDisabled(string text) { unsafe diff --git a/Dalamud/Interface/GameFonts/GameFontManager.cs b/Dalamud/Interface/GameFonts/GameFontManager.cs deleted file mode 100644 index b3454e085..000000000 --- a/Dalamud/Interface/GameFonts/GameFontManager.cs +++ /dev/null @@ -1,507 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Numerics; -using System.Runtime.InteropServices; -using System.Text; -using System.Threading.Tasks; - -using Dalamud.Data; -using Dalamud.Game; -using Dalamud.Interface.Internal; -using Dalamud.Interface.Utility; -using Dalamud.Utility.Timing; -using ImGuiNET; -using Lumina.Data.Files; -using Serilog; - -using static Dalamud.Interface.Utility.ImGuiHelpers; - -namespace Dalamud.Interface.GameFonts; - -/// -/// Loads game font for use in ImGui. -/// -[ServiceManager.BlockingEarlyLoadedService] -internal class GameFontManager : IServiceType -{ - private static readonly string?[] FontNames = - { - null, - "AXIS_96", "AXIS_12", "AXIS_14", "AXIS_18", "AXIS_36", - "Jupiter_16", "Jupiter_20", "Jupiter_23", "Jupiter_45", "Jupiter_46", "Jupiter_90", - "Meidinger_16", "Meidinger_20", "Meidinger_40", - "MiedingerMid_10", "MiedingerMid_12", "MiedingerMid_14", "MiedingerMid_18", "MiedingerMid_36", - "TrumpGothic_184", "TrumpGothic_23", "TrumpGothic_34", "TrumpGothic_68", - }; - - private readonly object syncRoot = new(); - - private readonly FdtReader?[] fdts; - private readonly List texturePixels; - private readonly Dictionary fonts = new(); - private readonly Dictionary fontUseCounter = new(); - private readonly Dictionary>> glyphRectIds = new(); - -#pragma warning disable CS0414 - private bool isBetweenBuildFontsAndRightAfterImGuiIoFontsBuild = false; -#pragma warning restore CS0414 - - [ServiceManager.ServiceConstructor] - private GameFontManager(DataManager dataManager) - { - using (Timings.Start("Getting fdt data")) - { - this.fdts = FontNames.Select(fontName => fontName == null ? null : new FdtReader(dataManager.GetFile($"common/font/{fontName}.fdt")!.Data)).ToArray(); - } - - using (Timings.Start("Getting texture data")) - { - var texTasks = Enumerable - .Range(1, 1 + this.fdts - .Where(x => x != null) - .Select(x => x.Glyphs.Select(y => y.TextureFileIndex).Max()) - .Max()) - .Select(x => dataManager.GetFile($"common/font/font{x}.tex")!) - .Select(x => new Task(Timings.AttachTimingHandle(() => x.ImageData!))) - .ToArray(); - foreach (var task in texTasks) - task.Start(); - this.texturePixels = texTasks.Select(x => x.GetAwaiter().GetResult()).ToList(); - } - } - - /// - /// Describe font into a string. - /// - /// Font to describe. - /// A string in a form of "FontName (NNNpt)". - public static string DescribeFont(GameFontFamilyAndSize font) - { - return font switch - { - GameFontFamilyAndSize.Undefined => "-", - GameFontFamilyAndSize.Axis96 => "AXIS (9.6pt)", - GameFontFamilyAndSize.Axis12 => "AXIS (12pt)", - GameFontFamilyAndSize.Axis14 => "AXIS (14pt)", - GameFontFamilyAndSize.Axis18 => "AXIS (18pt)", - GameFontFamilyAndSize.Axis36 => "AXIS (36pt)", - GameFontFamilyAndSize.Jupiter16 => "Jupiter (16pt)", - GameFontFamilyAndSize.Jupiter20 => "Jupiter (20pt)", - GameFontFamilyAndSize.Jupiter23 => "Jupiter (23pt)", - GameFontFamilyAndSize.Jupiter45 => "Jupiter Numeric (45pt)", - GameFontFamilyAndSize.Jupiter46 => "Jupiter (46pt)", - GameFontFamilyAndSize.Jupiter90 => "Jupiter Numeric (90pt)", - GameFontFamilyAndSize.Meidinger16 => "Meidinger Numeric (16pt)", - GameFontFamilyAndSize.Meidinger20 => "Meidinger Numeric (20pt)", - GameFontFamilyAndSize.Meidinger40 => "Meidinger Numeric (40pt)", - GameFontFamilyAndSize.MiedingerMid10 => "MiedingerMid (10pt)", - GameFontFamilyAndSize.MiedingerMid12 => "MiedingerMid (12pt)", - GameFontFamilyAndSize.MiedingerMid14 => "MiedingerMid (14pt)", - GameFontFamilyAndSize.MiedingerMid18 => "MiedingerMid (18pt)", - GameFontFamilyAndSize.MiedingerMid36 => "MiedingerMid (36pt)", - GameFontFamilyAndSize.TrumpGothic184 => "Trump Gothic (18.4pt)", - GameFontFamilyAndSize.TrumpGothic23 => "Trump Gothic (23pt)", - GameFontFamilyAndSize.TrumpGothic34 => "Trump Gothic (34pt)", - GameFontFamilyAndSize.TrumpGothic68 => "Trump Gothic (68pt)", - _ => throw new ArgumentOutOfRangeException(nameof(font), font, "Invalid argument"), - }; - } - - /// - /// Determines whether a font should be able to display most of stuff. - /// - /// Font to check. - /// True if it can. - public static bool IsGenericPurposeFont(GameFontFamilyAndSize font) - { - return font switch - { - GameFontFamilyAndSize.Axis96 => true, - GameFontFamilyAndSize.Axis12 => true, - GameFontFamilyAndSize.Axis14 => true, - GameFontFamilyAndSize.Axis18 => true, - GameFontFamilyAndSize.Axis36 => true, - _ => false, - }; - } - - /// - /// Unscales fonts after they have been rendered onto atlas. - /// - /// Font to unscale. - /// Scale factor. - /// Whether to call target.BuildLookupTable(). - public static void UnscaleFont(ImFontPtr fontPtr, float fontScale, bool rebuildLookupTable = true) - { - if (fontScale == 1) - return; - - unsafe - { - var font = fontPtr.NativePtr; - for (int i = 0, i_ = font->IndexedHotData.Size; i < i_; ++i) - { - font->IndexedHotData.Ref(i).AdvanceX /= fontScale; - font->IndexedHotData.Ref(i).OccupiedWidth /= fontScale; - } - - font->FontSize /= fontScale; - font->Ascent /= fontScale; - font->Descent /= fontScale; - if (font->ConfigData != null) - font->ConfigData->SizePixels /= fontScale; - var glyphs = (ImFontGlyphReal*)font->Glyphs.Data; - for (int i = 0, i_ = font->Glyphs.Size; i < i_; i++) - { - var glyph = &glyphs[i]; - glyph->X0 /= fontScale; - glyph->X1 /= fontScale; - glyph->Y0 /= fontScale; - glyph->Y1 /= fontScale; - glyph->AdvanceX /= fontScale; - } - - for (int i = 0, i_ = font->KerningPairs.Size; i < i_; i++) - font->KerningPairs.Ref(i).AdvanceXAdjustment /= fontScale; - for (int i = 0, i_ = font->FrequentKerningPairs.Size; i < i_; i++) - font->FrequentKerningPairs.Ref(i) /= fontScale; - } - - if (rebuildLookupTable && fontPtr.Glyphs.Size > 0) - fontPtr.BuildLookupTableNonstandard(); - } - - /// - /// Create a glyph range for use with ImGui AddFont. - /// - /// Font family and size. - /// Merge two ranges into one if distance is below the value specified in this parameter. - /// Glyph ranges. - public GCHandle ToGlyphRanges(GameFontFamilyAndSize family, int mergeDistance = 8) - { - var fdt = this.fdts[(int)family]!; - var ranges = new List(fdt.Glyphs.Count) - { - checked((ushort)fdt.Glyphs[0].CharInt), - checked((ushort)fdt.Glyphs[0].CharInt), - }; - - foreach (var glyph in fdt.Glyphs.Skip(1)) - { - var c32 = glyph.CharInt; - if (c32 >= 0x10000) - break; - - var c16 = unchecked((ushort)c32); - if (ranges[^1] + mergeDistance >= c16 && c16 > ranges[^1]) - { - ranges[^1] = c16; - } - else if (ranges[^1] + 1 < c16) - { - ranges.Add(c16); - ranges.Add(c16); - } - } - - return GCHandle.Alloc(ranges.ToArray(), GCHandleType.Pinned); - } - - /// - /// Creates a new GameFontHandle, and increases internal font reference counter, and if it's first time use, then the font will be loaded on next font building process. - /// - /// Font to use. - /// Handle to game font that may or may not be ready yet. - public GameFontHandle NewFontRef(GameFontStyle style) - { - var interfaceManager = Service.Get(); - var needRebuild = false; - - lock (this.syncRoot) - { - this.fontUseCounter[style] = this.fontUseCounter.GetValueOrDefault(style, 0) + 1; - } - - needRebuild = !this.fonts.ContainsKey(style); - if (needRebuild) - { - Log.Information("[GameFontManager] NewFontRef: Queueing RebuildFonts because {0} has been requested.", style.ToString()); - Service.GetAsync() - .ContinueWith(task => task.Result.RunOnTick(() => interfaceManager.RebuildFonts())); - } - - return new(this, style); - } - - /// - /// Gets the font. - /// - /// Font to get. - /// Corresponding font or null. - public ImFontPtr? GetFont(GameFontStyle style) => this.fonts.GetValueOrDefault(style, null); - - /// - /// Gets the corresponding FdtReader. - /// - /// Font to get. - /// Corresponding FdtReader or null. - public FdtReader? GetFdtReader(GameFontFamilyAndSize family) => this.fdts[(int)family]; - - /// - /// Fills missing glyphs in target font from source font, if both are not null. - /// - /// Source font. - /// Target font. - /// Whether to copy missing glyphs only. - /// Whether to call target.BuildLookupTable(). - public void CopyGlyphsAcrossFonts(ImFontPtr? source, GameFontStyle target, bool missingOnly, bool rebuildLookupTable) - { - ImGuiHelpers.CopyGlyphsAcrossFonts(source ?? default, this.fonts[target], missingOnly, rebuildLookupTable); - } - - /// - /// Fills missing glyphs in target font from source font, if both are not null. - /// - /// Source font. - /// Target font. - /// Whether to copy missing glyphs only. - /// Whether to call target.BuildLookupTable(). - public void CopyGlyphsAcrossFonts(GameFontStyle source, ImFontPtr? target, bool missingOnly, bool rebuildLookupTable) - { - ImGuiHelpers.CopyGlyphsAcrossFonts(this.fonts[source], target ?? default, missingOnly, rebuildLookupTable); - } - - /// - /// Fills missing glyphs in target font from source font, if both are not null. - /// - /// Source font. - /// Target font. - /// Whether to copy missing glyphs only. - /// Whether to call target.BuildLookupTable(). - public void CopyGlyphsAcrossFonts(GameFontStyle source, GameFontStyle target, bool missingOnly, bool rebuildLookupTable) - { - ImGuiHelpers.CopyGlyphsAcrossFonts(this.fonts[source], this.fonts[target], missingOnly, rebuildLookupTable); - } - - /// - /// Build fonts before plugins do something more. To be called from InterfaceManager. - /// - public void BuildFonts() - { - this.isBetweenBuildFontsAndRightAfterImGuiIoFontsBuild = true; - - this.glyphRectIds.Clear(); - this.fonts.Clear(); - - lock (this.syncRoot) - { - foreach (var style in this.fontUseCounter.Keys) - this.EnsureFont(style); - } - } - - /// - /// Record that ImGui.GetIO().Fonts.Build() has been called. - /// - public void AfterIoFontsBuild() - { - this.isBetweenBuildFontsAndRightAfterImGuiIoFontsBuild = false; - } - - /// - /// Checks whether GameFontMamager owns an ImFont. - /// - /// ImFontPtr to check. - /// Whether it owns. - public bool OwnsFont(ImFontPtr fontPtr) => this.fonts.ContainsValue(fontPtr); - - /// - /// Post-build fonts before plugins do something more. To be called from InterfaceManager. - /// - public unsafe void AfterBuildFonts() - { - var interfaceManager = Service.Get(); - var ioFonts = ImGui.GetIO().Fonts; - var fontGamma = interfaceManager.FontGamma; - - var pixels8s = new byte*[ioFonts.Textures.Size]; - var pixels32s = new uint*[ioFonts.Textures.Size]; - var widths = new int[ioFonts.Textures.Size]; - var heights = new int[ioFonts.Textures.Size]; - for (var i = 0; i < pixels8s.Length; i++) - { - ioFonts.GetTexDataAsRGBA32(i, out pixels8s[i], out widths[i], out heights[i]); - pixels32s[i] = (uint*)pixels8s[i]; - } - - foreach (var (style, font) in this.fonts) - { - var fdt = this.fdts[(int)style.FamilyAndSize]; - var scale = style.SizePt / fdt.FontHeader.Size; - var fontPtr = font.NativePtr; - - Log.Verbose("[GameFontManager] AfterBuildFonts: Scaling {0} from {1}pt to {2}pt (scale: {3})", style.ToString(), fdt.FontHeader.Size, style.SizePt, scale); - - fontPtr->FontSize = fdt.FontHeader.Size * 4 / 3; - if (fontPtr->ConfigData != null) - fontPtr->ConfigData->SizePixels = fontPtr->FontSize; - fontPtr->Ascent = fdt.FontHeader.Ascent; - fontPtr->Descent = fdt.FontHeader.Descent; - fontPtr->EllipsisChar = '…'; - foreach (var fallbackCharCandidate in "〓?!") - { - var glyph = font.FindGlyphNoFallback(fallbackCharCandidate); - if ((IntPtr)glyph.NativePtr != IntPtr.Zero) - { - var ptr = font.NativePtr; - ptr->FallbackChar = fallbackCharCandidate; - ptr->FallbackGlyph = glyph.NativePtr; - ptr->FallbackHotData = (ImFontGlyphHotData*)ptr->IndexedHotData.Address(fallbackCharCandidate); - break; - } - } - - // I have no idea what's causing NPE, so just to be safe - try - { - if (font.NativePtr != null && font.NativePtr->ConfigData != null) - { - var nameBytes = Encoding.UTF8.GetBytes(style.ToString() + "\0"); - Marshal.Copy(nameBytes, 0, (IntPtr)font.ConfigData.Name.Data, Math.Min(nameBytes.Length, font.ConfigData.Name.Count)); - } - } - catch (NullReferenceException) - { - // do nothing - } - - foreach (var (c, (rectId, glyph)) in this.glyphRectIds[style]) - { - var rc = (ImFontAtlasCustomRectReal*)ioFonts.GetCustomRectByIndex(rectId).NativePtr; - var pixels8 = pixels8s[rc->TextureIndex]; - var pixels32 = pixels32s[rc->TextureIndex]; - var width = widths[rc->TextureIndex]; - var height = heights[rc->TextureIndex]; - var sourceBuffer = this.texturePixels[glyph.TextureFileIndex]; - var sourceBufferDelta = glyph.TextureChannelByteIndex; - var widthAdjustment = style.CalculateBaseWidthAdjustment(fdt, glyph); - if (widthAdjustment == 0) - { - for (var y = 0; y < glyph.BoundingHeight; y++) - { - for (var x = 0; x < glyph.BoundingWidth; x++) - { - var a = sourceBuffer[sourceBufferDelta + (4 * (((glyph.TextureOffsetY + y) * fdt.FontHeader.TextureWidth) + glyph.TextureOffsetX + x))]; - pixels32[((rc->Y + y) * width) + rc->X + x] = (uint)(a << 24) | 0xFFFFFFu; - } - } - } - else - { - for (var y = 0; y < glyph.BoundingHeight; y++) - { - for (var x = 0; x < glyph.BoundingWidth + widthAdjustment; x++) - pixels32[((rc->Y + y) * width) + rc->X + x] = 0xFFFFFFu; - } - - for (int xbold = 0, xbold_ = Math.Max(1, (int)Math.Ceiling(style.Weight + 1)); xbold < xbold_; xbold++) - { - var boldStrength = Math.Min(1f, style.Weight + 1 - xbold); - for (var y = 0; y < glyph.BoundingHeight; y++) - { - float xDelta = xbold; - if (style.BaseSkewStrength > 0) - xDelta += style.BaseSkewStrength * (fdt.FontHeader.LineHeight - glyph.CurrentOffsetY - y) / fdt.FontHeader.LineHeight; - else if (style.BaseSkewStrength < 0) - xDelta -= style.BaseSkewStrength * (glyph.CurrentOffsetY + y) / fdt.FontHeader.LineHeight; - var xDeltaInt = (int)Math.Floor(xDelta); - var xness = xDelta - xDeltaInt; - for (var x = 0; x < glyph.BoundingWidth; x++) - { - var sourcePixelIndex = ((glyph.TextureOffsetY + y) * fdt.FontHeader.TextureWidth) + glyph.TextureOffsetX + x; - var a1 = sourceBuffer[sourceBufferDelta + (4 * sourcePixelIndex)]; - var a2 = x == glyph.BoundingWidth - 1 ? 0 : sourceBuffer[sourceBufferDelta + (4 * (sourcePixelIndex + 1))]; - var n = (a1 * xness) + (a2 * (1 - xness)); - var targetOffset = ((rc->Y + y) * width) + rc->X + x + xDeltaInt; - pixels8[(targetOffset * 4) + 3] = Math.Max(pixels8[(targetOffset * 4) + 3], (byte)(boldStrength * n)); - } - } - } - } - - if (Math.Abs(fontGamma - 1.4f) >= 0.001) - { - // Gamma correction (stbtt/FreeType would output in linear space whereas most real world usages will apply 1.4 or 1.8 gamma; Windows/XIV prebaked uses 1.4) - for (int y = rc->Y, y_ = rc->Y + rc->Height; y < y_; y++) - { - for (int x = rc->X, x_ = rc->X + rc->Width; x < x_; x++) - { - var i = (((y * width) + x) * 4) + 3; - pixels8[i] = (byte)(Math.Pow(pixels8[i] / 255.0f, 1.4f / fontGamma) * 255.0f); - } - } - } - } - - UnscaleFont(font, 1 / scale, false); - } - } - - /// - /// Decrease font reference counter. - /// - /// Font to release. - internal void DecreaseFontRef(GameFontStyle style) - { - lock (this.syncRoot) - { - if (!this.fontUseCounter.ContainsKey(style)) - return; - - if ((this.fontUseCounter[style] -= 1) == 0) - this.fontUseCounter.Remove(style); - } - } - - private unsafe void EnsureFont(GameFontStyle style) - { - var rectIds = this.glyphRectIds[style] = new(); - - var fdt = this.fdts[(int)style.FamilyAndSize]; - if (fdt == null) - return; - - ImFontConfigPtr fontConfig = ImGuiNative.ImFontConfig_ImFontConfig(); - fontConfig.OversampleH = 1; - fontConfig.OversampleV = 1; - fontConfig.PixelSnapH = false; - - var io = ImGui.GetIO(); - var font = io.Fonts.AddFontDefault(fontConfig); - - fontConfig.Destroy(); - - this.fonts[style] = font; - foreach (var glyph in fdt.Glyphs) - { - var c = glyph.Char; - if (c < 32 || c >= 0xFFFF) - continue; - - var widthAdjustment = style.CalculateBaseWidthAdjustment(fdt, glyph); - rectIds[c] = Tuple.Create( - io.Fonts.AddCustomRectFontGlyph( - font, - c, - glyph.BoundingWidth + widthAdjustment, - glyph.BoundingHeight, - glyph.AdvanceWidth, - new Vector2(0, glyph.CurrentOffsetY)), - glyph); - } - - foreach (var kernPair in fdt.Distances) - font.AddKerningPair(kernPair.Left, kernPair.Right, kernPair.RightOffset); - } -} diff --git a/Dalamud/Interface/GameFonts/GameFontStyle.cs b/Dalamud/Interface/GameFonts/GameFontStyle.cs index 946473df4..fbaf9de07 100644 --- a/Dalamud/Interface/GameFonts/GameFontStyle.cs +++ b/Dalamud/Interface/GameFonts/GameFontStyle.cs @@ -64,7 +64,7 @@ public struct GameFontStyle /// public float SizePt { - get => this.SizePx * 3 / 4; + readonly get => this.SizePx * 3 / 4; set => this.SizePx = value * 4 / 3; } @@ -73,14 +73,14 @@ public struct GameFontStyle /// public float BaseSkewStrength { - get => this.SkewStrength * this.BaseSizePx / this.SizePx; + readonly get => this.SkewStrength * this.BaseSizePx / this.SizePx; set => this.SkewStrength = value * this.SizePx / this.BaseSizePx; } /// /// Gets the font family. /// - public GameFontFamily Family => this.FamilyAndSize switch + public readonly GameFontFamily Family => this.FamilyAndSize switch { GameFontFamilyAndSize.Undefined => GameFontFamily.Undefined, GameFontFamilyAndSize.Axis96 => GameFontFamily.Axis, @@ -112,7 +112,7 @@ public struct GameFontStyle /// /// Gets the corresponding GameFontFamilyAndSize but with minimum possible font sizes. /// - public GameFontFamilyAndSize FamilyWithMinimumSize => this.Family switch + public readonly GameFontFamilyAndSize FamilyWithMinimumSize => this.Family switch { GameFontFamily.Axis => GameFontFamilyAndSize.Axis96, GameFontFamily.Jupiter => GameFontFamilyAndSize.Jupiter16, @@ -126,7 +126,7 @@ public struct GameFontStyle /// /// Gets the base font size in point unit. /// - public float BaseSizePt => this.FamilyAndSize switch + public readonly float BaseSizePt => this.FamilyAndSize switch { GameFontFamilyAndSize.Undefined => 0, GameFontFamilyAndSize.Axis96 => 9.6f, @@ -158,14 +158,14 @@ public struct GameFontStyle /// /// Gets the base font size in pixel unit. /// - public float BaseSizePx => this.BaseSizePt * 4 / 3; + public readonly float BaseSizePx => this.BaseSizePt * 4 / 3; /// /// Gets or sets a value indicating whether this font is bold. /// public bool Bold { - get => this.Weight > 0f; + readonly get => this.Weight > 0f; set => this.Weight = value ? 1f : 0f; } @@ -174,8 +174,8 @@ public struct GameFontStyle /// public bool Italic { - get => this.SkewStrength != 0; - set => this.SkewStrength = value ? this.SizePx / 7 : 0; + readonly get => this.SkewStrength != 0; + set => this.SkewStrength = value ? this.SizePx / 6 : 0; } /// @@ -233,13 +233,26 @@ public struct GameFontStyle _ => GameFontFamilyAndSize.Undefined, }; + /// + /// Creates a new scaled instance of struct. + /// + /// The scale. + /// The scaled instance. + public readonly GameFontStyle Scale(float scale) => new() + { + FamilyAndSize = GetRecommendedFamilyAndSize(this.Family, this.SizePt * scale), + SizePx = this.SizePx * scale, + Weight = this.Weight, + SkewStrength = this.SkewStrength * scale, + }; + /// /// Calculates the adjustment to width resulting fron Weight and SkewStrength. /// /// Font header. /// Glyph. /// Width adjustment in pixel unit. - public int CalculateBaseWidthAdjustment(in FdtReader.FontTableHeader header, in FdtReader.FontTableEntry glyph) + public readonly int CalculateBaseWidthAdjustment(in FdtReader.FontTableHeader header, in FdtReader.FontTableEntry glyph) { var widthDelta = this.Weight; switch (this.BaseSkewStrength) @@ -263,11 +276,11 @@ public struct GameFontStyle /// Font information. /// Glyph. /// Width adjustment in pixel unit. - public int CalculateBaseWidthAdjustment(FdtReader reader, FdtReader.FontTableEntry glyph) => + public readonly int CalculateBaseWidthAdjustment(FdtReader reader, FdtReader.FontTableEntry glyph) => this.CalculateBaseWidthAdjustment(reader.FontHeader, glyph); /// - public override string ToString() + public override readonly string ToString() { return $"GameFontStyle({this.FamilyAndSize}, {this.SizePt}pt, skew={this.SkewStrength}, weight={this.Weight})"; } diff --git a/Dalamud/Interface/Internal/DalamudIme.cs b/Dalamud/Interface/Internal/DalamudIme.cs index e030b4e50..28a9075bd 100644 --- a/Dalamud/Interface/Internal/DalamudIme.cs +++ b/Dalamud/Interface/Internal/DalamudIme.cs @@ -11,6 +11,7 @@ using System.Text.Unicode; using Dalamud.Game.Text; using Dalamud.Hooking.WndProcHook; using Dalamud.Interface.GameFonts; +using Dalamud.Interface.ManagedFontAtlas.Internals; using Dalamud.Interface.Utility; using ImGuiNET; @@ -196,9 +197,9 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType { if (HanRange.Any(x => x.FirstCodePoint <= chr && chr < x.FirstCodePoint + x.Length)) { - if (Service.Get() - .GetFdtReader(GameFontFamilyAndSize.Axis12) - ?.FindGlyph(chr) is null) + if (Service.Get() + ?.GetFdtReader(GameFontFamilyAndSize.Axis12) + .FindGlyph(chr) is null) { if (!this.EncounteredHan) { diff --git a/Dalamud/Interface/Internal/DalamudInterface.cs b/Dalamud/Interface/Internal/DalamudInterface.cs index 95415659b..60c1f9957 100644 --- a/Dalamud/Interface/Internal/DalamudInterface.cs +++ b/Dalamud/Interface/Internal/DalamudInterface.cs @@ -21,6 +21,7 @@ using Dalamud.Interface.Internal.Windows.PluginInstaller; using Dalamud.Interface.Internal.Windows.SelfTest; using Dalamud.Interface.Internal.Windows.Settings; using Dalamud.Interface.Internal.Windows.StyleEditor; +using Dalamud.Interface.ManagedFontAtlas.Internals; using Dalamud.Interface.Style; using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; @@ -93,7 +94,8 @@ internal class DalamudInterface : IDisposable, IServiceType private DalamudInterface( Dalamud dalamud, DalamudConfiguration configuration, - InterfaceManager.InterfaceManagerWithScene interfaceManagerWithScene, + FontAtlasFactory fontAtlasFactory, + InterfaceManager interfaceManager, PluginImageCache pluginImageCache, DalamudAssetManager dalamudAssetManager, Game.Framework framework, @@ -103,7 +105,7 @@ internal class DalamudInterface : IDisposable, IServiceType { this.dalamud = dalamud; this.configuration = configuration; - this.interfaceManager = interfaceManagerWithScene.Manager; + this.interfaceManager = interfaceManager; this.WindowSystem = new WindowSystem("DalamudCore"); @@ -122,10 +124,14 @@ internal class DalamudInterface : IDisposable, IServiceType clientState, configuration, dalamudAssetManager, + fontAtlasFactory, framework, gameGui, titleScreenMenu) { IsOpen = false }; - this.changelogWindow = new ChangelogWindow(this.titleScreenMenuWindow) { IsOpen = false }; + this.changelogWindow = new ChangelogWindow( + this.titleScreenMenuWindow, + fontAtlasFactory, + dalamudAssetManager) { IsOpen = false }; this.profilerWindow = new ProfilerWindow() { IsOpen = false }; this.branchSwitcherWindow = new BranchSwitcherWindow() { IsOpen = false }; this.hitchSettingsWindow = new HitchSettingsWindow() { IsOpen = false }; @@ -207,6 +213,7 @@ internal class DalamudInterface : IDisposable, IServiceType { this.interfaceManager.Draw -= this.OnDraw; + this.WindowSystem.Windows.OfType().AggregateToDisposable().Dispose(); this.WindowSystem.RemoveAllWindows(); this.changelogWindow.Dispose(); diff --git a/Dalamud/Interface/Internal/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs index 48157fa86..3e004727a 100644 --- a/Dalamud/Interface/Internal/InterfaceManager.cs +++ b/Dalamud/Interface/Internal/InterfaceManager.cs @@ -1,13 +1,10 @@ -using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; -using System.Text; -using System.Text.Unicode; -using System.Threading; +using System.Threading.Tasks; using Dalamud.Configuration.Internal; using Dalamud.Game; @@ -19,10 +16,13 @@ using Dalamud.Hooking.WndProcHook; using Dalamud.Interface.GameFonts; using Dalamud.Interface.Internal.ManagedAsserts; using Dalamud.Interface.Internal.Notifications; +using Dalamud.Interface.ManagedFontAtlas; +using Dalamud.Interface.ManagedFontAtlas.Internals; using Dalamud.Interface.Style; using Dalamud.Interface.Utility; using Dalamud.Interface.Windowing; -using Dalamud.Storage.Assets; +using Dalamud.Plugin.Internal; +using Dalamud.Plugin.Internal.Types; using Dalamud.Utility; using Dalamud.Utility.Timing; using ImGuiNET; @@ -64,11 +64,9 @@ internal class InterfaceManager : IDisposable, IServiceType /// public const float DefaultFontSizePx = (DefaultFontSizePt * 4.0f) / 3.0f; - private const ushort Fallback1Codepoint = 0x3013; // Geta mark; FFXIV uses this to indicate that a glyph is missing. - private const ushort Fallback2Codepoint = '-'; // FFXIV uses dash if Geta mark is unavailable. - - private readonly HashSet glyphRequests = new(); - private readonly Dictionary loadedFontInfo = new(); + private const int NonMainThreadFontAccessWarningCheckInterval = 10000; + private static readonly ConditionalWeakTable NonMainThreadFontAccessWarning = new(); + private static long nextNonMainThreadFontAccessWarningCheck; private readonly List deferredDisposeTextures = new(); @@ -81,28 +79,28 @@ internal class InterfaceManager : IDisposable, IServiceType [ServiceManager.ServiceDependency] private readonly DalamudIme dalamudIme = Service.Get(); - private readonly ManualResetEvent fontBuildSignal; - private readonly SwapChainVtableResolver address; + private readonly SwapChainVtableResolver address = new(); private readonly Hook setCursorHook; private RawDX11Scene? scene; private Hook? presentHook; private Hook? resizeBuffersHook; + private IFontAtlas? dalamudAtlas; + private IFontHandle.IInternal? defaultFontHandle; + private IFontHandle.IInternal? iconFontHandle; + private IFontHandle.IInternal? monoFontHandle; + // can't access imgui IO before first present call private bool lastWantCapture = false; - private bool isRebuildingFonts = false; private bool isOverrideGameCursor = true; + private IntPtr gameWindowHandle; [ServiceManager.ServiceConstructor] private InterfaceManager() { this.setCursorHook = Hook.FromImport( null, "user32.dll", "SetCursor", 0, this.SetCursorDetour); - - this.fontBuildSignal = new ManualResetEvent(false); - - this.address = new SwapChainVtableResolver(); } [UnmanagedFunctionPointer(CallingConvention.ThisCall)] @@ -117,43 +115,46 @@ internal class InterfaceManager : IDisposable, IServiceType /// /// This event gets called each frame to facilitate ImGui drawing. /// - public event RawDX11Scene.BuildUIDelegate Draw; + public event RawDX11Scene.BuildUIDelegate? Draw; /// /// This event gets called when ResizeBuffers is called. /// - public event Action ResizeBuffers; - - /// - /// Gets or sets an action that is executed right before fonts are rebuilt. - /// - public event Action BuildFonts; + public event Action? ResizeBuffers; /// /// Gets or sets an action that is executed right after fonts are rebuilt. /// - public event Action AfterBuildFonts; + public event Action? AfterBuildFonts; /// - /// Gets the default ImGui font. + /// Gets the default ImGui font.
+ /// Accessing this static property outside of the main thread is dangerous and not supported. ///
- public static ImFontPtr DefaultFont { get; private set; } + public static ImFontPtr DefaultFont => WhenFontsReady().defaultFontHandle!.ImFont.OrElse(ImGui.GetIO().FontDefault); /// - /// Gets an included FontAwesome icon font. + /// Gets an included FontAwesome icon font.
+ /// Accessing this static property outside of the main thread is dangerous and not supported. ///
- public static ImFontPtr IconFont { get; private set; } + public static ImFontPtr IconFont => WhenFontsReady().iconFontHandle!.ImFont.OrElse(ImGui.GetIO().FontDefault); /// - /// Gets an included monospaced font. + /// Gets an included monospaced font.
+ /// Accessing this static property outside of the main thread is dangerous and not supported. ///
- public static ImFontPtr MonoFont { get; private set; } + public static ImFontPtr MonoFont => WhenFontsReady().monoFontHandle!.ImFont.OrElse(ImGui.GetIO().FontDefault); /// /// Gets or sets the pointer to ImGui.IO(), when it was last used. /// public ImGuiIOPtr LastImGuiIoPtr { get; set; } + /// + /// Gets the DX11 scene. + /// + public RawDX11Scene? Scene => this.scene; + /// /// Gets the D3D11 device instance. /// @@ -178,11 +179,6 @@ internal class InterfaceManager : IDisposable, IServiceType } } - /// - /// Gets or sets a value indicating whether the fonts are built and ready to use. - /// - public bool FontsReady { get; set; } = false; - /// /// Gets a value indicating whether the Dalamud interface ready to use. /// @@ -193,50 +189,57 @@ internal class InterfaceManager : IDisposable, IServiceType /// public bool IsDispatchingEvents { get; set; } = true; - /// - /// Gets or sets a value indicating whether to override configuration for UseAxis. - /// - public bool? UseAxisOverride { get; set; } = null; - - /// - /// Gets a value indicating whether to use AXIS fonts. - /// - public bool UseAxis => this.UseAxisOverride ?? Service.Get().UseAxisFontsFromGame; - - /// - /// Gets or sets the overrided font gamma value, instead of using the value from configuration. - /// - public float? FontGammaOverride { get; set; } = null; - - /// - /// Gets the font gamma value to use. - /// - public float FontGamma => Math.Max(0.1f, this.FontGammaOverride.GetValueOrDefault(Service.Get().FontGammaLevel)); - - /// - /// Gets a value indicating whether we're building fonts but haven't generated atlas yet. - /// - public bool IsBuildingFontsBeforeAtlasBuild => this.isRebuildingFonts && !this.fontBuildSignal.WaitOne(0); - /// /// Gets a value indicating the native handle of the game main window. /// - public IntPtr GameWindowHandle { get; private set; } + public IntPtr GameWindowHandle + { + get + { + if (this.gameWindowHandle == 0) + { + nint gwh = 0; + while ((gwh = NativeFunctions.FindWindowEx(0, gwh, "FFXIVGAME", 0)) != 0) + { + _ = User32.GetWindowThreadProcessId(gwh, out var pid); + if (pid == Environment.ProcessId && User32.IsWindowVisible(gwh)) + { + this.gameWindowHandle = gwh; + break; + } + } + } + + return this.gameWindowHandle; + } + } + + /// + /// Gets the font build task. + /// + public Task FontBuildTask => WhenFontsReady().dalamudAtlas!.BuildTask; /// /// Dispose of managed and unmanaged resources. /// public void Dispose() { - this.framework.RunOnFrameworkThread(() => + if (Service.GetNullable() is { } framework) + framework.RunOnFrameworkThread(Disposer).Wait(); + else + Disposer(); + + this.wndProcHookManager.PreWndProc -= this.WndProcHookManagerOnPreWndProc; + this.dalamudAtlas?.Dispose(); + this.scene?.Dispose(); + return; + + void Disposer() { this.setCursorHook.Dispose(); this.presentHook?.Dispose(); this.resizeBuffersHook?.Dispose(); - }).Wait(); - - this.wndProcHookManager.PreWndProc -= this.WndProcHookManagerOnPreWndProc; - this.scene?.Dispose(); + } } #nullable enable @@ -376,93 +379,8 @@ internal class InterfaceManager : IDisposable, IServiceType /// public void RebuildFonts() { - if (this.scene == null) - { - Log.Verbose("[FONT] RebuildFonts(): scene not ready, doing nothing"); - return; - } - Log.Verbose("[FONT] RebuildFonts() called"); - - // don't invoke this multiple times per frame, in case multiple plugins call it - if (!this.isRebuildingFonts) - { - Log.Verbose("[FONT] RebuildFonts() trigger"); - this.isRebuildingFonts = true; - this.scene.OnNewRenderFrame += this.RebuildFontsInternal; - } - } - - /// - /// Wait for the rebuilding fonts to complete. - /// - public void WaitForFontRebuild() - { - this.fontBuildSignal.WaitOne(); - } - - /// - /// Requests a default font of specified size to exist. - /// - /// Font size in pixels. - /// Ranges of glyphs. - /// Requets handle. - public SpecialGlyphRequest NewFontSizeRef(float size, List> ranges) - { - var allContained = false; - var fonts = ImGui.GetIO().Fonts.Fonts; - ImFontPtr foundFont = null; - unsafe - { - for (int i = 0, i_ = fonts.Size; i < i_; i++) - { - if (!this.glyphRequests.Any(x => x.FontInternal.NativePtr == fonts[i].NativePtr)) - continue; - - allContained = true; - foreach (var range in ranges) - { - if (!allContained) - break; - - for (var j = range.Item1; j <= range.Item2 && allContained; j++) - allContained &= fonts[i].FindGlyphNoFallback(j).NativePtr != null; - } - - if (allContained) - foundFont = fonts[i]; - - break; - } - } - - var req = new SpecialGlyphRequest(this, size, ranges); - req.FontInternal = foundFont; - - if (!allContained) - this.RebuildFonts(); - - return req; - } - - /// - /// Requests a default font of specified size to exist. - /// - /// Font size in pixels. - /// Text to calculate glyph ranges from. - /// Requets handle. - public SpecialGlyphRequest NewFontSizeRef(float size, string text) - { - List> ranges = new(); - foreach (var c in new SortedSet(text.ToHashSet())) - { - if (ranges.Any() && ranges[^1].Item2 + 1 == c) - ranges[^1] = Tuple.Create(ranges[^1].Item1, c); - else - ranges.Add(Tuple.Create(c, c)); - } - - return this.NewFontSizeRef(size, ranges); + this.dalamudAtlas?.BuildFontsAsync(); } /// @@ -486,11 +404,11 @@ internal class InterfaceManager : IDisposable, IServiceType try { var dxgiDev = this.Device.QueryInterfaceOrNull(); - var dxgiAdapter = dxgiDev?.Adapter.QueryInterfaceOrNull(); + var dxgiAdapter = dxgiDev?.Adapter.QueryInterfaceOrNull(); if (dxgiAdapter == null) return null; - var memInfo = dxgiAdapter.QueryVideoMemoryInfo(0, SharpDX.DXGI.MemorySegmentGroup.Local); + var memInfo = dxgiAdapter.QueryVideoMemoryInfo(0, MemorySegmentGroup.Local); return (memInfo.CurrentUsage, memInfo.CurrentReservation); } catch @@ -516,20 +434,65 @@ internal class InterfaceManager : IDisposable, IServiceType /// Value. internal void SetImmersiveMode(bool enabled) { - if (this.GameWindowHandle == nint.Zero) - return; - - int value = enabled ? 1 : 0; - var hr = NativeFunctions.DwmSetWindowAttribute( - this.GameWindowHandle, - NativeFunctions.DWMWINDOWATTRIBUTE.DWMWA_USE_IMMERSIVE_DARK_MODE, - ref value, - sizeof(int)); + if (this.GameWindowHandle == 0) + throw new InvalidOperationException("Game window is not yet ready."); + var value = enabled ? 1 : 0; + ((Result)NativeFunctions.DwmSetWindowAttribute( + this.GameWindowHandle, + NativeFunctions.DWMWINDOWATTRIBUTE.DWMWA_USE_IMMERSIVE_DARK_MODE, + ref value, + sizeof(int))).CheckError(); } - private static void ShowFontError(string path) + private static InterfaceManager WhenFontsReady() { - Util.Fatal($"One or more files required by XIVLauncher were not found.\nPlease restart and report this error if it occurs again.\n\n{path}", "Error"); + var im = Service.GetNullable(); + if (im?.dalamudAtlas is not { } atlas) + throw new InvalidOperationException($"Tried to access fonts before {nameof(ContinueConstruction)} call."); + + if (!ThreadSafety.IsMainThread && nextNonMainThreadFontAccessWarningCheck < Environment.TickCount64) + { + nextNonMainThreadFontAccessWarningCheck = + Environment.TickCount64 + NonMainThreadFontAccessWarningCheckInterval; + var stack = new StackTrace(); + if (Service.GetNullable()?.FindCallingPlugin(stack) is { } plugin) + { + if (!NonMainThreadFontAccessWarning.TryGetValue(plugin, out _)) + { + NonMainThreadFontAccessWarning.Add(plugin, new()); + Log.Warning( + "[IM] {pluginName}: Accessing fonts outside the main thread is deprecated.\n{stack}", + plugin.Name, + stack); + } + } + else + { + // Dalamud internal should be made safe right now + throw new InvalidOperationException("Attempted to access fonts outside the main thread."); + } + } + + if (!atlas.HasBuiltAtlas) + atlas.BuildTask.GetAwaiter().GetResult(); + return im; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void RenderImGui(RawDX11Scene scene) + { + var conf = Service.Get(); + + // Process information needed by ImGuiHelpers each frame. + ImGuiHelpers.NewFrame(); + + // Enable viewports if there are no issues. + if (conf.IsDisableViewport || scene.SwapChain.IsFullScreen || ImGui.GetPlatformIO().Monitors.Size == 1) + ImGui.GetIO().ConfigFlags &= ~ImGuiConfigFlags.ViewportsEnable; + else + ImGui.GetIO().ConfigFlags |= ImGuiConfigFlags.ViewportsEnable; + + scene.Render(); } private void InitScene(IntPtr swapChain) @@ -546,7 +509,7 @@ internal class InterfaceManager : IDisposable, IServiceType Service.ProvideException(ex); Log.Error(ex, "Could not load ImGui dependencies."); - var res = PInvoke.User32.MessageBox( + var res = User32.MessageBox( IntPtr.Zero, "Dalamud plugins require the Microsoft Visual C++ Redistributable to be installed.\nPlease install the runtime from the official Microsoft website or disable Dalamud.\n\nDo you want to download the redistributable now?", "Dalamud Error", @@ -578,7 +541,7 @@ internal class InterfaceManager : IDisposable, IServiceType if (iniFileInfo.Length > 1200000) { Log.Warning("dalamudUI.ini was over 1mb, deleting"); - iniFileInfo.CopyTo(Path.Combine(iniFileInfo.DirectoryName, $"dalamudUI-{DateTimeOffset.Now.ToUnixTimeSeconds()}.ini")); + iniFileInfo.CopyTo(Path.Combine(iniFileInfo.DirectoryName!, $"dalamudUI-{DateTimeOffset.Now.ToUnixTimeSeconds()}.ini")); iniFileInfo.Delete(); } } @@ -623,8 +586,6 @@ internal class InterfaceManager : IDisposable, IServiceType ImGui.GetIO().FontGlobalScale = configuration.GlobalUiScale; - this.SetupFonts(); - if (!configuration.IsDocking) { ImGui.GetIO().ConfigFlags &= ~ImGuiConfigFlags.DockingEnable; @@ -675,26 +636,34 @@ internal class InterfaceManager : IDisposable, IServiceType */ private IntPtr PresentDetour(IntPtr swapChain, uint syncInterval, uint presentFlags) { + Debug.Assert(this.presentHook is not null, "How did PresentDetour get called when presentHook is null?"); + Debug.Assert(this.dalamudAtlas is not null, "dalamudAtlas should have been set already"); + if (this.scene != null && swapChain != this.scene.SwapChain.NativePointer) return this.presentHook!.Original(swapChain, syncInterval, presentFlags); if (this.scene == null) this.InitScene(swapChain); + Debug.Assert(this.scene is not null, "InitScene did not set the scene field, but did not throw an exception."); + + if (!this.dalamudAtlas!.HasBuiltAtlas) + return this.presentHook!.Original(swapChain, syncInterval, presentFlags); + if (this.address.IsReshade) { - var pRes = this.presentHook.Original(swapChain, syncInterval, presentFlags); + var pRes = this.presentHook!.Original(swapChain, syncInterval, presentFlags); - this.RenderImGui(); + RenderImGui(this.scene!); this.DisposeTextures(); return pRes; } - this.RenderImGui(); + RenderImGui(this.scene!); this.DisposeTextures(); - return this.presentHook.Original(swapChain, syncInterval, presentFlags); + return this.presentHook!.Original(swapChain, syncInterval, presentFlags); } private void DisposeTextures() @@ -711,471 +680,73 @@ internal class InterfaceManager : IDisposable, IServiceType } } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void RenderImGui() + [ServiceManager.CallWhenServicesReady( + "InterfaceManager accepts event registration and stuff even when the game window is not ready.")] + private void ContinueConstruction( + TargetSigScanner sigScanner, + Framework framework, + FontAtlasFactory fontAtlasFactory) { - // Process information needed by ImGuiHelpers each frame. - ImGuiHelpers.NewFrame(); - - // Check if we can still enable viewports without any issues. - this.CheckViewportState(); - - this.scene.Render(); - } - - private void CheckViewportState() - { - var configuration = Service.Get(); - - if (configuration.IsDisableViewport || this.scene.SwapChain.IsFullScreen || ImGui.GetPlatformIO().Monitors.Size == 1) + this.dalamudAtlas = fontAtlasFactory + .CreateFontAtlas(nameof(InterfaceManager), FontAtlasAutoRebuildMode.Disable); + using (this.dalamudAtlas.SuppressAutoRebuild()) { - ImGui.GetIO().ConfigFlags &= ~ImGuiConfigFlags.ViewportsEnable; - return; + this.defaultFontHandle = (IFontHandle.IInternal)this.dalamudAtlas.NewDelegateFontHandle( + e => e.OnPreBuild(tk => tk.AddDalamudDefaultFont(DefaultFontSizePx))); + this.iconFontHandle = (IFontHandle.IInternal)this.dalamudAtlas.NewDelegateFontHandle( + e => e.OnPreBuild( + tk => tk.AddFontAwesomeIconFont( + new() + { + SizePx = DefaultFontSizePx, + GlyphMinAdvanceX = DefaultFontSizePx, + GlyphMaxAdvanceX = DefaultFontSizePx, + }))); + this.monoFontHandle = (IFontHandle.IInternal)this.dalamudAtlas.NewDelegateFontHandle( + e => e.OnPreBuild( + tk => tk.AddDalamudAssetFont( + DalamudAsset.InconsolataRegular, + new() { SizePx = DefaultFontSizePx }))); + this.dalamudAtlas.BuildStepChange += e => e.OnPostPromotion( + tk => + { + // Note: the first call of this function is done outside the main thread; this is expected. + // Do not use DefaultFont, IconFont, and MonoFont. + // Use font handles directly. + + // Fill missing glyphs in MonoFont from DefaultFont + tk.CopyGlyphsAcrossFonts(this.defaultFontHandle.ImFont, this.monoFontHandle.ImFont, true); + + // Broadcast to auto-rebuilding instances + this.AfterBuildFonts?.Invoke(); + }); } - ImGui.GetIO().ConfigFlags |= ImGuiConfigFlags.ViewportsEnable; - } + // This will wait for scene on its own. We just wait for this.dalamudAtlas.BuildTask in this.InitScene. + _ = this.dalamudAtlas.BuildFontsAsync(false); - /// - /// Loads font for use in ImGui text functions. - /// - private unsafe void SetupFonts() - { - using var setupFontsTimings = Timings.Start("IM SetupFonts"); - - var gameFontManager = Service.Get(); - var dalamud = Service.Get(); - var io = ImGui.GetIO(); - var ioFonts = io.Fonts; - - var fontGamma = this.FontGamma; - - this.fontBuildSignal.Reset(); - ioFonts.Clear(); - ioFonts.TexDesiredWidth = 4096; - - Log.Verbose("[FONT] SetupFonts - 1"); - - foreach (var v in this.loadedFontInfo) - v.Value.Dispose(); - - this.loadedFontInfo.Clear(); - - Log.Verbose("[FONT] SetupFonts - 2"); - - ImFontConfigPtr fontConfig = null; - List garbageList = new(); + this.address.Setup(sigScanner); try { - var dummyRangeHandle = GCHandle.Alloc(new ushort[] { '0', '0', 0 }, GCHandleType.Pinned); - garbageList.Add(dummyRangeHandle); - - fontConfig = ImGuiNative.ImFontConfig_ImFontConfig(); - fontConfig.OversampleH = 1; - fontConfig.OversampleV = 1; - - var fontPathJp = Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "NotoSansCJKjp-Regular.otf"); - if (!File.Exists(fontPathJp)) - fontPathJp = Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "NotoSansCJKjp-Medium.otf"); - if (!File.Exists(fontPathJp)) - ShowFontError(fontPathJp); - Log.Verbose("[FONT] fontPathJp = {0}", fontPathJp); - - var fontPathKr = Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "NotoSansCJKkr-Regular.otf"); - if (!File.Exists(fontPathKr)) - fontPathKr = Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "NotoSansKR-Regular.otf"); - if (!File.Exists(fontPathKr)) - fontPathKr = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Windows), "Fonts", "malgun.ttf"); - if (!File.Exists(fontPathKr)) - fontPathKr = null; - Log.Verbose("[FONT] fontPathKr = {0}", fontPathKr); - - var fontPathChs = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Windows), "Fonts", "msyh.ttc"); - if (!File.Exists(fontPathChs)) - fontPathChs = null; - Log.Verbose("[FONT] fontPathChs = {0}", fontPathChs); - - var fontPathCht = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Windows), "Fonts", "msjh.ttc"); - if (!File.Exists(fontPathCht)) - fontPathCht = null; - Log.Verbose("[FONT] fontPathChs = {0}", fontPathCht); - - // Default font - Log.Verbose("[FONT] SetupFonts - Default font"); - var fontInfo = new TargetFontModification( - "Default", - this.UseAxis ? TargetFontModification.AxisMode.Overwrite : TargetFontModification.AxisMode.GameGlyphsOnly, - this.UseAxis ? DefaultFontSizePx : DefaultFontSizePx + 1, - io.FontGlobalScale); - Log.Verbose("[FONT] SetupFonts - Default corresponding AXIS size: {0}pt ({1}px)", fontInfo.SourceAxis.Style.BaseSizePt, fontInfo.SourceAxis.Style.BaseSizePx); - fontConfig.SizePixels = fontInfo.TargetSizePx * io.FontGlobalScale; - if (this.UseAxis) - { - fontConfig.GlyphRanges = dummyRangeHandle.AddrOfPinnedObject(); - fontConfig.PixelSnapH = false; - DefaultFont = ioFonts.AddFontDefault(fontConfig); - this.loadedFontInfo[DefaultFont] = fontInfo; - } - else - { - var rangeHandle = gameFontManager.ToGlyphRanges(GameFontFamilyAndSize.Axis12); - garbageList.Add(rangeHandle); - - fontConfig.GlyphRanges = rangeHandle.AddrOfPinnedObject(); - fontConfig.PixelSnapH = true; - DefaultFont = ioFonts.AddFontFromFileTTF(fontPathJp, fontConfig.SizePixels, fontConfig); - this.loadedFontInfo[DefaultFont] = fontInfo; - } - - if (fontPathKr != null - && (Service.Get().EffectiveLanguage == "ko" || this.dalamudIme.EncounteredHangul)) - { - fontConfig.MergeMode = true; - fontConfig.GlyphRanges = ioFonts.GetGlyphRangesKorean(); - fontConfig.PixelSnapH = true; - ioFonts.AddFontFromFileTTF(fontPathKr, fontConfig.SizePixels, fontConfig); - fontConfig.MergeMode = false; - } - - if (fontPathCht != null && Service.Get().EffectiveLanguage == "tw") - { - fontConfig.MergeMode = true; - var rangeHandle = GCHandle.Alloc(new ushort[] - { - (ushort)UnicodeRanges.CjkUnifiedIdeographs.FirstCodePoint, - (ushort)(UnicodeRanges.CjkUnifiedIdeographs.FirstCodePoint + - (UnicodeRanges.CjkUnifiedIdeographs.Length - 1)), - (ushort)UnicodeRanges.CjkUnifiedIdeographsExtensionA.FirstCodePoint, - (ushort)(UnicodeRanges.CjkUnifiedIdeographsExtensionA.FirstCodePoint + - (UnicodeRanges.CjkUnifiedIdeographsExtensionA.Length - 1)), - 0, - }, GCHandleType.Pinned); - garbageList.Add(rangeHandle); - fontConfig.GlyphRanges = rangeHandle.AddrOfPinnedObject(); - fontConfig.PixelSnapH = true; - ioFonts.AddFontFromFileTTF(fontPathCht, fontConfig.SizePixels, fontConfig); - fontConfig.MergeMode = false; - } - else if (fontPathChs != null && (Service.Get().EffectiveLanguage == "zh" - || this.dalamudIme.EncounteredHan)) - { - fontConfig.MergeMode = true; - var rangeHandle = GCHandle.Alloc(new ushort[] - { - (ushort)UnicodeRanges.CjkUnifiedIdeographs.FirstCodePoint, - (ushort)(UnicodeRanges.CjkUnifiedIdeographs.FirstCodePoint + - (UnicodeRanges.CjkUnifiedIdeographs.Length - 1)), - (ushort)UnicodeRanges.CjkUnifiedIdeographsExtensionA.FirstCodePoint, - (ushort)(UnicodeRanges.CjkUnifiedIdeographsExtensionA.FirstCodePoint + - (UnicodeRanges.CjkUnifiedIdeographsExtensionA.Length - 1)), - 0, - }, GCHandleType.Pinned); - garbageList.Add(rangeHandle); - fontConfig.GlyphRanges = rangeHandle.AddrOfPinnedObject(); - fontConfig.PixelSnapH = true; - ioFonts.AddFontFromFileTTF(fontPathChs, fontConfig.SizePixels, fontConfig); - fontConfig.MergeMode = false; - } - - // FontAwesome icon font - Log.Verbose("[FONT] SetupFonts - FontAwesome icon font"); - { - var fontPathIcon = Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "FontAwesomeFreeSolid.otf"); - if (!File.Exists(fontPathIcon)) - ShowFontError(fontPathIcon); - - var iconRangeHandle = GCHandle.Alloc(new ushort[] { 0xE000, 0xF8FF, 0, }, GCHandleType.Pinned); - garbageList.Add(iconRangeHandle); - - fontConfig.GlyphRanges = iconRangeHandle.AddrOfPinnedObject(); - fontConfig.PixelSnapH = true; - IconFont = ioFonts.AddFontFromFileTTF(fontPathIcon, DefaultFontSizePx * io.FontGlobalScale, fontConfig); - this.loadedFontInfo[IconFont] = new("Icon", TargetFontModification.AxisMode.GameGlyphsOnly, DefaultFontSizePx, io.FontGlobalScale); - } - - // Monospace font - Log.Verbose("[FONT] SetupFonts - Monospace font"); - { - var fontPathMono = Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "Inconsolata-Regular.ttf"); - if (!File.Exists(fontPathMono)) - ShowFontError(fontPathMono); - - fontConfig.GlyphRanges = IntPtr.Zero; - fontConfig.PixelSnapH = true; - MonoFont = ioFonts.AddFontFromFileTTF(fontPathMono, DefaultFontSizePx * io.FontGlobalScale, fontConfig); - this.loadedFontInfo[MonoFont] = new("Mono", TargetFontModification.AxisMode.GameGlyphsOnly, DefaultFontSizePx, io.FontGlobalScale); - } - - // Default font but in requested size for requested glyphs - Log.Verbose("[FONT] SetupFonts - Default font but in requested size for requested glyphs"); - { - Dictionary> extraFontRequests = new(); - foreach (var extraFontRequest in this.glyphRequests) - { - if (!extraFontRequests.ContainsKey(extraFontRequest.Size)) - extraFontRequests[extraFontRequest.Size] = new(); - extraFontRequests[extraFontRequest.Size].Add(extraFontRequest); - } - - foreach (var (fontSize, requests) in extraFontRequests) - { - List<(ushort, ushort)> codepointRanges = new(4 + requests.Sum(x => x.CodepointRanges.Count)) - { - new(Fallback1Codepoint, Fallback1Codepoint), - new(Fallback2Codepoint, Fallback2Codepoint), - // ImGui default ellipsis characters - new(0x2026, 0x2026), - new(0x0085, 0x0085), - }; - - foreach (var request in requests) - codepointRanges.AddRange(request.CodepointRanges.Select(x => (From: x.Item1, To: x.Item2))); - - codepointRanges.Sort(); - List flattenedRanges = new(); - foreach (var range in codepointRanges) - { - if (flattenedRanges.Any() && flattenedRanges[^1] >= range.Item1 - 1) - { - flattenedRanges[^1] = Math.Max(flattenedRanges[^1], range.Item2); - } - else - { - flattenedRanges.Add(range.Item1); - flattenedRanges.Add(range.Item2); - } - } - - flattenedRanges.Add(0); - - fontInfo = new( - $"Requested({fontSize}px)", - this.UseAxis ? TargetFontModification.AxisMode.Overwrite : TargetFontModification.AxisMode.GameGlyphsOnly, - fontSize, - io.FontGlobalScale); - if (this.UseAxis) - { - fontConfig.GlyphRanges = dummyRangeHandle.AddrOfPinnedObject(); - fontConfig.SizePixels = fontInfo.SourceAxis.Style.BaseSizePx; - fontConfig.PixelSnapH = false; - - var sizedFont = ioFonts.AddFontDefault(fontConfig); - this.loadedFontInfo[sizedFont] = fontInfo; - foreach (var request in requests) - request.FontInternal = sizedFont; - } - else - { - var rangeHandle = GCHandle.Alloc(flattenedRanges.ToArray(), GCHandleType.Pinned); - garbageList.Add(rangeHandle); - fontConfig.PixelSnapH = true; - - var sizedFont = ioFonts.AddFontFromFileTTF(fontPathJp, fontSize * io.FontGlobalScale, fontConfig, rangeHandle.AddrOfPinnedObject()); - this.loadedFontInfo[sizedFont] = fontInfo; - foreach (var request in requests) - request.FontInternal = sizedFont; - } - } - } - - gameFontManager.BuildFonts(); - - var customFontFirstConfigIndex = ioFonts.ConfigData.Size; - - Log.Verbose("[FONT] Invoke OnBuildFonts"); - this.BuildFonts?.InvokeSafely(); - Log.Verbose("[FONT] OnBuildFonts OK!"); - - for (int i = customFontFirstConfigIndex, i_ = ioFonts.ConfigData.Size; i < i_; i++) - { - var config = ioFonts.ConfigData[i]; - if (gameFontManager.OwnsFont(config.DstFont)) - continue; - - config.OversampleH = 1; - config.OversampleV = 1; - - var name = Encoding.UTF8.GetString((byte*)config.Name.Data, config.Name.Count).TrimEnd('\0'); - if (name.IsNullOrEmpty()) - name = $"{config.SizePixels}px"; - - // ImFont information is reflected only if corresponding ImFontConfig has MergeMode not set. - if (config.MergeMode) - { - if (!this.loadedFontInfo.ContainsKey(config.DstFont.NativePtr)) - { - Log.Warning("MergeMode specified for {0} but not found in loadedFontInfo. Skipping.", name); - continue; - } - } - else - { - if (this.loadedFontInfo.ContainsKey(config.DstFont.NativePtr)) - { - Log.Warning("MergeMode not specified for {0} but found in loadedFontInfo. Skipping.", name); - continue; - } - - // While the font will be loaded in the scaled size after FontScale is applied, the font will be treated as having the requested size when used from plugins. - this.loadedFontInfo[config.DstFont.NativePtr] = new($"PlReq({name})", config.SizePixels); - } - - config.SizePixels = config.SizePixels * io.FontGlobalScale; - } - - for (int i = 0, i_ = ioFonts.ConfigData.Size; i < i_; i++) - { - var config = ioFonts.ConfigData[i]; - config.RasterizerGamma *= fontGamma; - } - - Log.Verbose("[FONT] ImGui.IO.Build will be called."); - ioFonts.Build(); - gameFontManager.AfterIoFontsBuild(); - this.ClearStacks(); - Log.Verbose("[FONT] ImGui.IO.Build OK!"); - - gameFontManager.AfterBuildFonts(); - - foreach (var (font, mod) in this.loadedFontInfo) - { - // I have no idea what's causing NPE, so just to be safe - try - { - if (font.NativePtr != null && font.NativePtr->ConfigData != null) - { - var nameBytes = Encoding.UTF8.GetBytes($"{mod.Name}\0"); - Marshal.Copy(nameBytes, 0, (IntPtr)font.ConfigData.Name.Data, Math.Min(nameBytes.Length, font.ConfigData.Name.Count)); - } - } - catch (NullReferenceException) - { - // do nothing - } - - Log.Verbose("[FONT] {0}: Unscale with scale value of {1}", mod.Name, mod.Scale); - GameFontManager.UnscaleFont(font, mod.Scale, false); - - if (mod.Axis == TargetFontModification.AxisMode.Overwrite) - { - Log.Verbose("[FONT] {0}: Overwrite from AXIS of size {1}px (was {2}px)", mod.Name, mod.SourceAxis.ImFont.FontSize, font.FontSize); - GameFontManager.UnscaleFont(font, font.FontSize / mod.SourceAxis.ImFont.FontSize, false); - var ascentDiff = mod.SourceAxis.ImFont.Ascent - font.Ascent; - font.Ascent += ascentDiff; - font.Descent = ascentDiff; - font.FallbackChar = mod.SourceAxis.ImFont.FallbackChar; - font.EllipsisChar = mod.SourceAxis.ImFont.EllipsisChar; - ImGuiHelpers.CopyGlyphsAcrossFonts(mod.SourceAxis.ImFont, font, false, false); - } - else if (mod.Axis == TargetFontModification.AxisMode.GameGlyphsOnly) - { - Log.Verbose("[FONT] {0}: Overwrite game specific glyphs from AXIS of size {1}px", mod.Name, mod.SourceAxis.ImFont.FontSize, font.FontSize); - if (!this.UseAxis && font.NativePtr == DefaultFont.NativePtr) - mod.SourceAxis.ImFont.FontSize -= 1; - ImGuiHelpers.CopyGlyphsAcrossFonts(mod.SourceAxis.ImFont, font, true, false, 0xE020, 0xE0DB); - if (!this.UseAxis && font.NativePtr == DefaultFont.NativePtr) - mod.SourceAxis.ImFont.FontSize += 1; - } - - Log.Verbose("[FONT] {0}: Resize from {1}px to {2}px", mod.Name, font.FontSize, mod.TargetSizePx); - GameFontManager.UnscaleFont(font, font.FontSize / mod.TargetSizePx, false); - } - - // Fill missing glyphs in MonoFont from DefaultFont - ImGuiHelpers.CopyGlyphsAcrossFonts(DefaultFont, MonoFont, true, false); - - for (int i = 0, i_ = ioFonts.Fonts.Size; i < i_; i++) - { - var font = ioFonts.Fonts[i]; - if (font.Glyphs.Size == 0) - { - Log.Warning("[FONT] Font has no glyph: {0}", font.GetDebugName()); - continue; - } - - if (font.FindGlyphNoFallback(Fallback1Codepoint).NativePtr != null) - font.FallbackChar = Fallback1Codepoint; - - font.BuildLookupTableNonstandard(); - } - - Log.Verbose("[FONT] Invoke OnAfterBuildFonts"); - this.AfterBuildFonts?.InvokeSafely(); - Log.Verbose("[FONT] OnAfterBuildFonts OK!"); - - if (ioFonts.Fonts[0].NativePtr != DefaultFont.NativePtr) - Log.Warning("[FONT] First font is not DefaultFont"); - - Log.Verbose("[FONT] Fonts built!"); - - this.fontBuildSignal.Set(); - - this.FontsReady = true; + if (Service.Get().WindowIsImmersive) + this.SetImmersiveMode(true); } - finally + catch (Exception ex) { - if (fontConfig.NativePtr != null) - fontConfig.Destroy(); - - foreach (var garbage in garbageList) - garbage.Free(); + Log.Error(ex, "Could not enable immersive mode"); } - } - [ServiceManager.CallWhenServicesReady( - "InterfaceManager accepts event registration and stuff even when the game window is not ready.")] - private void ContinueConstruction(TargetSigScanner sigScanner, DalamudConfiguration configuration) - { - this.address.Setup(sigScanner); - this.framework.RunOnFrameworkThread(() => - { - while ((this.GameWindowHandle = NativeFunctions.FindWindowEx(IntPtr.Zero, this.GameWindowHandle, "FFXIVGAME", IntPtr.Zero)) != IntPtr.Zero) - { - _ = User32.GetWindowThreadProcessId(this.GameWindowHandle, out var pid); + this.presentHook = Hook.FromAddress(this.address.Present, this.PresentDetour); + this.resizeBuffersHook = Hook.FromAddress(this.address.ResizeBuffers, this.ResizeBuffersDetour); - if (pid == Environment.ProcessId && User32.IsWindowVisible(this.GameWindowHandle)) - break; - } + Log.Verbose("===== S W A P C H A I N ====="); + Log.Verbose($"Present address 0x{this.presentHook!.Address.ToInt64():X}"); + Log.Verbose($"ResizeBuffers address 0x{this.resizeBuffersHook!.Address.ToInt64():X}"); - try - { - if (configuration.WindowIsImmersive) - this.SetImmersiveMode(true); - } - catch (Exception ex) - { - Log.Error(ex, "Could not enable immersive mode"); - } - - this.presentHook = Hook.FromAddress(this.address.Present, this.PresentDetour); - this.resizeBuffersHook = Hook.FromAddress(this.address.ResizeBuffers, this.ResizeBuffersDetour); - - Log.Verbose("===== S W A P C H A I N ====="); - Log.Verbose($"Present address 0x{this.presentHook!.Address.ToInt64():X}"); - Log.Verbose($"ResizeBuffers address 0x{this.resizeBuffersHook!.Address.ToInt64():X}"); - - this.setCursorHook.Enable(); - this.presentHook.Enable(); - this.resizeBuffersHook.Enable(); - }); - } - - // This is intended to only be called as a handler attached to scene.OnNewRenderFrame - private void RebuildFontsInternal() - { - Log.Verbose("[FONT] RebuildFontsInternal() called"); - this.SetupFonts(); - - Log.Verbose("[FONT] RebuildFontsInternal() detaching"); - this.scene!.OnNewRenderFrame -= this.RebuildFontsInternal; - - Log.Verbose("[FONT] Calling InvalidateFonts"); - this.scene.InvalidateFonts(); - - Log.Verbose("[FONT] Font Rebuild OK!"); - - this.isRebuildingFonts = false; + this.setCursorHook.Enable(); + this.presentHook.Enable(); + this.resizeBuffersHook.Enable(); } private IntPtr ResizeBuffersDetour(IntPtr swapChain, uint bufferCount, uint width, uint height, uint newFormat, uint swapChainFlags) @@ -1206,14 +777,17 @@ internal class InterfaceManager : IDisposable, IServiceType private IntPtr SetCursorDetour(IntPtr hCursor) { - if (this.lastWantCapture == true && (!this.scene?.IsImGuiCursor(hCursor) ?? false) && this.OverrideGameCursor) + if (this.lastWantCapture && (!this.scene?.IsImGuiCursor(hCursor) ?? false) && this.OverrideGameCursor) return IntPtr.Zero; - return this.setCursorHook.IsDisposed ? User32.SetCursor(new User32.SafeCursorHandle(hCursor, false)).DangerousGetHandle() : this.setCursorHook.Original(hCursor); + return this.setCursorHook.IsDisposed + ? User32.SetCursor(new(hCursor, false)).DangerousGetHandle() + : this.setCursorHook.Original(hCursor); } private void OnNewInputFrame() { + var io = ImGui.GetIO(); var dalamudInterface = Service.GetNullable(); var gamepadState = Service.GetNullable(); var keyState = Service.GetNullable(); @@ -1221,18 +795,21 @@ internal class InterfaceManager : IDisposable, IServiceType if (dalamudInterface == null || gamepadState == null || keyState == null) return; + // Prevent setting the footgun from ImGui Demo; the Space key isn't removing the flag at the moment. + io.ConfigFlags &= ~ImGuiConfigFlags.NoMouse; + // fix for keys in game getting stuck, if you were holding a game key (like run) // and then clicked on an imgui textbox - imgui would swallow the keyup event, // so the game would think the key remained pressed continuously until you left // imgui and pressed and released the key again - if (ImGui.GetIO().WantTextInput) + if (io.WantTextInput) { keyState.ClearAll(); } // TODO: mouse state? - var gamepadEnabled = (ImGui.GetIO().BackendFlags & ImGuiBackendFlags.HasGamepad) > 0; + var gamepadEnabled = (io.BackendFlags & ImGuiBackendFlags.HasGamepad) > 0; // NOTE (Chiv) Activate ImGui navigation via L1+L3 press // (mimicking how mouse navigation is activated via L1+R3 press in game). @@ -1240,12 +817,12 @@ internal class InterfaceManager : IDisposable, IServiceType && gamepadState.Raw(GamepadButtons.L1) > 0 && gamepadState.Pressed(GamepadButtons.L3) > 0) { - ImGui.GetIO().ConfigFlags ^= ImGuiConfigFlags.NavEnableGamepad; + io.ConfigFlags ^= ImGuiConfigFlags.NavEnableGamepad; gamepadState.NavEnableGamepad ^= true; dalamudInterface.ToggleGamepadModeNotifierWindow(); } - if (gamepadEnabled && (ImGui.GetIO().ConfigFlags & ImGuiConfigFlags.NavEnableGamepad) > 0) + if (gamepadEnabled && (io.ConfigFlags & ImGuiConfigFlags.NavEnableGamepad) > 0) { var northButton = gamepadState.Raw(GamepadButtons.North) != 0; var eastButton = gamepadState.Raw(GamepadButtons.East) != 0; @@ -1264,7 +841,6 @@ internal class InterfaceManager : IDisposable, IServiceType var r1Button = gamepadState.Raw(GamepadButtons.R1) != 0; var r2Button = gamepadState.Raw(GamepadButtons.R2) != 0; - var io = ImGui.GetIO(); io.AddKeyEvent(ImGuiKey.GamepadFaceUp, northButton); io.AddKeyEvent(ImGuiKey.GamepadFaceRight, eastButton); io.AddKeyEvent(ImGuiKey.GamepadFaceDown, southButton); @@ -1312,7 +888,10 @@ internal class InterfaceManager : IDisposable, IServiceType var snap = ImGuiManagedAsserts.GetSnapshot(); if (this.IsDispatchingEvents) - this.Draw?.Invoke(); + { + using (this.defaultFontHandle?.Push()) + this.Draw?.Invoke(); + } ImGuiManagedAsserts.ReportProblems("Dalamud Core", snap); @@ -1339,123 +918,4 @@ internal class InterfaceManager : IDisposable, IServiceType /// public InterfaceManager Manager { get; init; } } - - /// - /// Represents a glyph request. - /// - public class SpecialGlyphRequest : IDisposable - { - /// - /// Initializes a new instance of the class. - /// - /// InterfaceManager to associate. - /// Font size in pixels. - /// Codepoint ranges. - internal SpecialGlyphRequest(InterfaceManager manager, float size, List> ranges) - { - this.Manager = manager; - this.Size = size; - this.CodepointRanges = ranges; - this.Manager.glyphRequests.Add(this); - } - - /// - /// Gets the font of specified size, or DefaultFont if it's not ready yet. - /// - public ImFontPtr Font - { - get - { - unsafe - { - return this.FontInternal.NativePtr == null ? DefaultFont : this.FontInternal; - } - } - } - - /// - /// Gets or sets the associated ImFont. - /// - internal ImFontPtr FontInternal { get; set; } - - /// - /// Gets associated InterfaceManager. - /// - internal InterfaceManager Manager { get; init; } - - /// - /// Gets font size. - /// - internal float Size { get; init; } - - /// - /// Gets codepoint ranges. - /// - internal List> CodepointRanges { get; init; } - - /// - public void Dispose() - { - this.Manager.glyphRequests.Remove(this); - } - } - - private unsafe class TargetFontModification : IDisposable - { - /// - /// Initializes a new instance of the class. - /// Constructs new target font modification information, assuming that AXIS fonts will not be applied. - /// - /// Name of the font to write to ImGui font information. - /// Target font size in pixels, which will not be considered for further scaling. - internal TargetFontModification(string name, float sizePx) - { - this.Name = name; - this.Axis = AxisMode.Suppress; - this.TargetSizePx = sizePx; - this.Scale = 1; - this.SourceAxis = null; - } - - /// - /// Initializes a new instance of the class. - /// Constructs new target font modification information. - /// - /// Name of the font to write to ImGui font information. - /// Whether and how to use AXIS fonts. - /// Target font size in pixels, which will not be considered for further scaling. - /// Font scale to be referred for loading AXIS font of appropriate size. - internal TargetFontModification(string name, AxisMode axis, float sizePx, float globalFontScale) - { - this.Name = name; - this.Axis = axis; - this.TargetSizePx = sizePx; - this.Scale = globalFontScale; - this.SourceAxis = Service.Get().NewFontRef(new(GameFontFamily.Axis, this.TargetSizePx * this.Scale)); - } - - internal enum AxisMode - { - Suppress, - GameGlyphsOnly, - Overwrite, - } - - internal string Name { get; private init; } - - internal AxisMode Axis { get; private init; } - - internal float TargetSizePx { get; private init; } - - internal float Scale { get; private init; } - - internal GameFontHandle? SourceAxis { get; private init; } - - internal bool SourceAxisAvailable => this.SourceAxis != null && this.SourceAxis.ImFont.NativePtr != null; - - public void Dispose() - { - this.SourceAxis?.Dispose(); - } - } } diff --git a/Dalamud/Interface/Internal/Windows/ChangelogWindow.cs b/Dalamud/Interface/Internal/Windows/ChangelogWindow.cs index b9e7ab686..ae59db36a 100644 --- a/Dalamud/Interface/Internal/Windows/ChangelogWindow.cs +++ b/Dalamud/Interface/Internal/Windows/ChangelogWindow.cs @@ -1,4 +1,3 @@ -using System.IO; using System.Linq; using System.Numerics; @@ -7,6 +6,8 @@ using Dalamud.Interface.Animation.EasingFunctions; using Dalamud.Interface.Colors; using Dalamud.Interface.Components; using Dalamud.Interface.GameFonts; +using Dalamud.Interface.ManagedFontAtlas; +using Dalamud.Interface.ManagedFontAtlas.Internals; using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; using Dalamud.Interface.Windowing; @@ -31,8 +32,14 @@ internal sealed class ChangelogWindow : Window, IDisposable • Plugins can now add tooltips and interaction to the server info bar • The Dalamud/plugin installer UI has been refreshed "; - + private readonly TitleScreenMenuWindow tsmWindow; + + private readonly DisposeSafety.ScopedFinalizer scopedFinalizer = new(); + private readonly IFontAtlas privateAtlas; + private readonly Lazy bannerFont; + private readonly Lazy apiBumpExplainerTexture; + private readonly Lazy logoTexture; private readonly InOutCubic windowFade = new(TimeSpan.FromSeconds(2.5f)) { @@ -46,27 +53,36 @@ internal sealed class ChangelogWindow : Window, IDisposable Point2 = Vector2.One, }; - private IDalamudTextureWrap? apiBumpExplainerTexture; - private IDalamudTextureWrap? logoTexture; - private GameFontHandle? bannerFont; - private State state = State.WindowFadeIn; private bool needFadeRestart = false; - + /// /// Initializes a new instance of the class. /// /// TSM window. - public ChangelogWindow(TitleScreenMenuWindow tsmWindow) + /// An instance of . + /// An instance of . + public ChangelogWindow( + TitleScreenMenuWindow tsmWindow, + FontAtlasFactory fontAtlasFactory, + DalamudAssetManager assets) : base("What's new in Dalamud?##ChangelogWindow", ImGuiWindowFlags.NoResize | ImGuiWindowFlags.NoTitleBar | ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.NoScrollWithMouse, true) { this.tsmWindow = tsmWindow; this.Namespace = "DalamudChangelogWindow"; + this.privateAtlas = this.scopedFinalizer.Add( + fontAtlasFactory.CreateFontAtlas(this.Namespace, FontAtlasAutoRebuildMode.Async)); + this.bannerFont = new( + () => this.scopedFinalizer.Add( + this.privateAtlas.NewGameFontHandle(new(GameFontFamilyAndSize.MiedingerMid18)))); + + this.apiBumpExplainerTexture = new(() => assets.GetDalamudTextureWrap(DalamudAsset.ChangelogApiBumpIcon)); + this.logoTexture = new(() => assets.GetDalamudTextureWrap(DalamudAsset.Logo)); // If we are going to show a changelog, make sure we have the font ready, otherwise it will hitch if (WarrantsChangelog()) - Service.GetAsync().ContinueWith(t => this.MakeFont(t.Result)); + _ = this.bannerFont.Value; } private enum State @@ -97,20 +113,12 @@ internal sealed class ChangelogWindow : Window, IDisposable Service.Get().SetCreditsDarkeningAnimation(true); this.tsmWindow.AllowDrawing = false; - this.MakeFont(Service.Get()); + _ = this.bannerFont; this.state = State.WindowFadeIn; this.windowFade.Reset(); this.bodyFade.Reset(); this.needFadeRestart = true; - - if (this.apiBumpExplainerTexture == null) - { - var dalamud = Service.Get(); - var tm = Service.Get(); - this.apiBumpExplainerTexture = tm.GetTextureFromFile(new FileInfo(Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "changelogApiBump.png"))) - ?? throw new Exception("Could not load api bump explainer."); - } base.OnOpen(); } @@ -186,10 +194,7 @@ internal sealed class ChangelogWindow : Window, IDisposable ImGui.SetCursorPos(new Vector2(logoContainerSize.X / 2 - logoSize.X / 2, logoContainerSize.Y / 2 - logoSize.Y / 2)); using (ImRaii.PushStyle(ImGuiStyleVar.Alpha, Math.Clamp(this.windowFade.EasedPoint.X - 0.5f, 0f, 1f))) - { - this.logoTexture ??= Service.Get().GetDalamudTextureWrap(DalamudAsset.Logo); - ImGui.Image(this.logoTexture.ImGuiHandle, logoSize); - } + ImGui.Image(this.logoTexture.Value.ImGuiHandle, logoSize); } ImGui.SameLine(); @@ -205,7 +210,7 @@ internal sealed class ChangelogWindow : Window, IDisposable using (ImRaii.PushStyle(ImGuiStyleVar.Alpha, Math.Clamp(this.windowFade.EasedPoint.X - 1f, 0f, 1f))) { - using var font = ImRaii.PushFont(this.bannerFont!.ImFont); + using var font = this.bannerFont.Value.Push(); switch (this.state) { @@ -275,9 +280,11 @@ internal sealed class ChangelogWindow : Window, IDisposable ImGui.TextWrapped("If some plugins are displayed with a red cross in the 'Installed Plugins' tab, they may not yet be available."); ImGuiHelpers.ScaledDummy(15); - - ImGuiHelpers.CenterCursorFor(this.apiBumpExplainerTexture!.Width); - ImGui.Image(this.apiBumpExplainerTexture.ImGuiHandle, this.apiBumpExplainerTexture.Size); + + ImGuiHelpers.CenterCursorFor(this.apiBumpExplainerTexture.Value.Width); + ImGui.Image( + this.apiBumpExplainerTexture.Value.ImGuiHandle, + this.apiBumpExplainerTexture.Value.Size); DrawNextButton(State.Links); break; @@ -377,7 +384,4 @@ internal sealed class ChangelogWindow : Window, IDisposable public void Dispose() { } - - private void MakeFont(GameFontManager gfm) => - this.bannerFont ??= gfm.NewFontRef(new GameFontStyle(GameFontFamilyAndSize.MiedingerMid18)); } diff --git a/Dalamud/Interface/Internal/Windows/Data/DataWindow.cs b/Dalamud/Interface/Internal/Windows/Data/DataWindow.cs index 20c3d6d01..951d3d91c 100644 --- a/Dalamud/Interface/Internal/Windows/Data/DataWindow.cs +++ b/Dalamud/Interface/Internal/Windows/Data/DataWindow.cs @@ -6,6 +6,8 @@ using Dalamud.Interface.Components; using Dalamud.Interface.Internal.Windows.Data.Widgets; using Dalamud.Interface.Utility; using Dalamud.Interface.Windowing; +using Dalamud.Utility; + using ImGuiNET; using Serilog; @@ -14,7 +16,7 @@ namespace Dalamud.Interface.Internal.Windows.Data; /// /// Class responsible for drawing the data/debug window. /// -internal class DataWindow : Window +internal class DataWindow : Window, IDisposable { private readonly IDataWindowWidget[] modules = { @@ -34,6 +36,7 @@ internal class DataWindow : Window new FlyTextWidget(), new FontAwesomeTestWidget(), new GameInventoryTestWidget(), + new GamePrebakedFontsTestWidget(), new GamepadWidget(), new GaugeWidget(), new HookWidget(), @@ -76,6 +79,9 @@ internal class DataWindow : Window this.Load(); } + /// + public void Dispose() => this.modules.OfType().AggregateToDisposable().Dispose(); + /// public override void OnOpen() { diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/GamePrebakedFontsTestWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/GamePrebakedFontsTestWidget.cs new file mode 100644 index 000000000..dba293e8b --- /dev/null +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/GamePrebakedFontsTestWidget.cs @@ -0,0 +1,213 @@ +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Text; + +using Dalamud.Interface.GameFonts; +using Dalamud.Interface.ManagedFontAtlas; +using Dalamud.Interface.ManagedFontAtlas.Internals; +using Dalamud.Interface.Utility; +using Dalamud.Utility; + +using ImGuiNET; + +namespace Dalamud.Interface.Internal.Windows.Data.Widgets; + +/// +/// Widget for testing game prebaked fonts. +/// +internal class GamePrebakedFontsTestWidget : IDataWindowWidget, IDisposable +{ + private ImVectorWrapper testStringBuffer; + private IFontAtlas? privateAtlas; + private IReadOnlyDictionary Handle)[]>? fontHandles; + private bool useGlobalScale; + private bool useWordWrap; + private bool useItalic; + private bool useBold; + private bool useMinimumBuild; + + /// + public string[]? CommandShortcuts { get; init; } + + /// + public string DisplayName { get; init; } = "Game Prebaked Fonts"; + + /// + public bool Ready { get; set; } + + /// + public void Load() => this.Ready = true; + + /// + public unsafe void Draw() + { + ImGui.AlignTextToFramePadding(); + fixed (byte* labelPtr = "Global Scale"u8) + { + var v = (byte)(this.useGlobalScale ? 1 : 0); + if (ImGuiNative.igCheckbox(labelPtr, &v) != 0) + { + this.useGlobalScale = v != 0; + this.ClearAtlas(); + } + } + + ImGui.SameLine(); + fixed (byte* labelPtr = "Word Wrap"u8) + { + var v = (byte)(this.useWordWrap ? 1 : 0); + if (ImGuiNative.igCheckbox(labelPtr, &v) != 0) + this.useWordWrap = v != 0; + } + + ImGui.SameLine(); + fixed (byte* labelPtr = "Italic"u8) + { + var v = (byte)(this.useItalic ? 1 : 0); + if (ImGuiNative.igCheckbox(labelPtr, &v) != 0) + { + this.useItalic = v != 0; + this.ClearAtlas(); + } + } + + ImGui.SameLine(); + fixed (byte* labelPtr = "Bold"u8) + { + var v = (byte)(this.useBold ? 1 : 0); + if (ImGuiNative.igCheckbox(labelPtr, &v) != 0) + { + this.useBold = v != 0; + this.ClearAtlas(); + } + } + + ImGui.SameLine(); + fixed (byte* labelPtr = "Minimum Range"u8) + { + var v = (byte)(this.useMinimumBuild ? 1 : 0); + if (ImGuiNative.igCheckbox(labelPtr, &v) != 0) + { + this.useMinimumBuild = v != 0; + this.ClearAtlas(); + } + } + + ImGui.SameLine(); + if (ImGui.Button("Reset Text") || this.testStringBuffer.IsDisposed) + { + this.testStringBuffer.Dispose(); + this.testStringBuffer = ImVectorWrapper.CreateFromSpan( + "(Game)-[Font] {Test}. 0123456789!! <氣気气きキ기>。"u8, + minCapacity: 1024); + } + + fixed (byte* labelPtr = "Test Input"u8) + { + if (ImGuiNative.igInputTextMultiline( + labelPtr, + this.testStringBuffer.Data, + (uint)this.testStringBuffer.Capacity, + new(ImGui.GetContentRegionAvail().X, 32 * ImGuiHelpers.GlobalScale), + 0, + null, + null) != 0) + { + var len = this.testStringBuffer.StorageSpan.IndexOf((byte)0); + if (len + 4 >= this.testStringBuffer.Capacity) + this.testStringBuffer.EnsureCapacityExponential(len + 4); + if (len < this.testStringBuffer.Capacity) + { + this.testStringBuffer.LengthUnsafe = len; + this.testStringBuffer.StorageSpan[len] = default; + } + + if (this.useMinimumBuild) + _ = this.privateAtlas?.BuildFontsAsync(); + } + } + + this.privateAtlas ??= + Service.Get().CreateFontAtlas( + nameof(GamePrebakedFontsTestWidget), + FontAtlasAutoRebuildMode.Async, + this.useGlobalScale); + this.fontHandles ??= + Enum.GetValues() + .Where(x => x.GetAttribute() is not null) + .Select(x => new GameFontStyle(x) { Italic = this.useItalic, Bold = this.useBold }) + .GroupBy(x => x.Family) + .ToImmutableDictionary( + x => x.Key, + x => x.Select( + y => (y, new Lazy( + () => this.useMinimumBuild + ? this.privateAtlas.NewDelegateFontHandle( + e => + e.OnPreBuild( + tk => tk.AddGameGlyphs( + y, + Encoding.UTF8.GetString( + this.testStringBuffer.DataSpan).ToGlyphRange(), + default))) + : this.privateAtlas.NewGameFontHandle(y)))) + .ToArray()); + + var offsetX = ImGui.CalcTextSize("99.9pt").X + (ImGui.GetStyle().FramePadding.X * 2); + foreach (var (family, items) in this.fontHandles) + { + if (!ImGui.CollapsingHeader($"{family} Family")) + continue; + + foreach (var (gfs, handle) in items) + { + ImGui.TextUnformatted($"{gfs.SizePt}pt"); + ImGui.SameLine(offsetX); + ImGuiNative.igPushTextWrapPos(this.useWordWrap ? 0f : -1f); + try + { + if (handle.Value.LoadException is { } exc) + { + ImGui.TextUnformatted(exc.ToString()); + } + else if (!handle.Value.Available) + { + fixed (byte* labelPtr = "Loading..."u8) + ImGuiNative.igTextUnformatted(labelPtr, labelPtr + 8 + ((Environment.TickCount / 200) % 3)); + } + else + { + if (!this.useGlobalScale) + ImGuiNative.igSetWindowFontScale(1 / ImGuiHelpers.GlobalScale); + using var pushPop = handle.Value.Push(); + ImGuiNative.igTextUnformatted( + this.testStringBuffer.Data, + this.testStringBuffer.Data + this.testStringBuffer.Length); + } + } + finally + { + ImGuiNative.igPopTextWrapPos(); + ImGuiNative.igSetWindowFontScale(1); + } + } + } + } + + /// + public void Dispose() + { + this.ClearAtlas(); + this.testStringBuffer.Dispose(); + } + + private void ClearAtlas() + { + this.fontHandles?.Values.SelectMany(x => x.Where(y => y.Handle.IsValueCreated).Select(y => y.Handle.Value)) + .AggregateToDisposable().Dispose(); + this.fontHandles = null; + this.privateAtlas?.Dispose(); + this.privateAtlas = null; + } +} diff --git a/Dalamud/Interface/Internal/Windows/Settings/SettingsWindow.cs b/Dalamud/Interface/Internal/Windows/Settings/SettingsWindow.cs index 7d4489f8d..027e1a571 100644 --- a/Dalamud/Interface/Internal/Windows/Settings/SettingsWindow.cs +++ b/Dalamud/Interface/Internal/Windows/Settings/SettingsWindow.cs @@ -5,10 +5,10 @@ using CheapLoc; using Dalamud.Configuration.Internal; using Dalamud.Interface.Colors; using Dalamud.Interface.Internal.Windows.Settings.Tabs; +using Dalamud.Interface.ManagedFontAtlas.Internals; using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; using Dalamud.Interface.Windowing; -using Dalamud.Plugin.Internal; using Dalamud.Utility; using ImGuiNET; @@ -19,14 +19,7 @@ namespace Dalamud.Interface.Internal.Windows.Settings; /// internal class SettingsWindow : Window { - private readonly SettingsTab[] tabs = - { - new SettingsTabGeneral(), - new SettingsTabLook(), - new SettingsTabDtr(), - new SettingsTabExperimental(), - new SettingsTabAbout(), - }; + private SettingsTab[]? tabs; private string searchInput = string.Empty; @@ -49,6 +42,15 @@ internal class SettingsWindow : Window /// public override void OnOpen() { + this.tabs ??= new SettingsTab[] + { + new SettingsTabGeneral(), + new SettingsTabLook(), + new SettingsTabDtr(), + new SettingsTabExperimental(), + new SettingsTabAbout(), + }; + foreach (var settingsTab in this.tabs) { settingsTab.Load(); @@ -64,15 +66,12 @@ internal class SettingsWindow : Window { var configuration = Service.Get(); var interfaceManager = Service.Get(); + var fontAtlasFactory = Service.Get(); - var rebuildFont = - ImGui.GetIO().FontGlobalScale != configuration.GlobalUiScale || - interfaceManager.FontGamma != configuration.FontGammaLevel || - interfaceManager.UseAxis != configuration.UseAxisFontsFromGame; + var rebuildFont = fontAtlasFactory.UseAxis != configuration.UseAxisFontsFromGame; ImGui.GetIO().FontGlobalScale = configuration.GlobalUiScale; - interfaceManager.FontGammaOverride = null; - interfaceManager.UseAxisOverride = null; + fontAtlasFactory.UseAxisOverride = null; if (rebuildFont) interfaceManager.RebuildFonts(); diff --git a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabAbout.cs b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabAbout.cs index 5b6f6b02f..8714fd666 100644 --- a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabAbout.cs +++ b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabAbout.cs @@ -1,13 +1,13 @@ -using System; -using System.Diagnostics; +using System.Diagnostics; using System.Diagnostics.CodeAnalysis; -using System.IO; using System.Linq; using System.Numerics; using CheapLoc; using Dalamud.Game.Gui; using Dalamud.Interface.GameFonts; +using Dalamud.Interface.ManagedFontAtlas; +using Dalamud.Interface.ManagedFontAtlas.Internals; using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; using Dalamud.Plugin.Internal; @@ -15,7 +15,6 @@ using Dalamud.Storage.Assets; using Dalamud.Utility; using FFXIVClientStructs.FFXIV.Client.Game.UI; using ImGuiNET; -using ImGuiScene; namespace Dalamud.Interface.Internal.Windows.Settings.Tabs; @@ -173,16 +172,21 @@ Contribute at: https://github.com/goatcorp/Dalamud "; private readonly Stopwatch creditsThrottler; + private readonly IFontAtlas privateAtlas; private string creditsText; private bool resetNow = false; private IDalamudTextureWrap? logoTexture; - private GameFontHandle? thankYouFont; + private IFontHandle? thankYouFont; public SettingsTabAbout() { this.creditsThrottler = new(); + + this.privateAtlas = Service + .Get() + .CreateFontAtlas(nameof(SettingsTabAbout), FontAtlasAutoRebuildMode.Async); } public override SettingsEntry[] Entries { get; } = { }; @@ -207,11 +211,7 @@ Contribute at: https://github.com/goatcorp/Dalamud this.creditsThrottler.Restart(); - if (this.thankYouFont == null) - { - var gfm = Service.Get(); - this.thankYouFont = gfm.NewFontRef(new GameFontStyle(GameFontFamilyAndSize.TrumpGothic34)); - } + this.thankYouFont ??= this.privateAtlas.NewGameFontHandle(new(GameFontFamilyAndSize.TrumpGothic34)); this.resetNow = true; @@ -269,14 +269,12 @@ Contribute at: https://github.com/goatcorp/Dalamud if (this.thankYouFont != null) { - ImGui.PushFont(this.thankYouFont.ImFont); + using var fontPush = this.thankYouFont.Push(); var thankYouLenX = ImGui.CalcTextSize(ThankYouText).X; ImGui.Dummy(new Vector2((windowX / 2) - (thankYouLenX / 2), 0f)); ImGui.SameLine(); ImGui.TextUnformatted(ThankYouText); - - ImGui.PopFont(); } ImGuiHelpers.ScaledDummy(0, windowSize.Y + 50f); @@ -305,9 +303,5 @@ Contribute at: https://github.com/goatcorp/Dalamud /// /// Disposes of managed and unmanaged resources. /// - public override void Dispose() - { - this.logoTexture?.Dispose(); - this.thankYouFont?.Dispose(); - } + public override void Dispose() => this.privateAtlas.Dispose(); } diff --git a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabLook.cs b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabLook.cs index 02e8ce789..5293e13c4 100644 --- a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabLook.cs +++ b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabLook.cs @@ -1,12 +1,14 @@ using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Numerics; +using System.Text; using CheapLoc; using Dalamud.Configuration.Internal; using Dalamud.Interface.Colors; using Dalamud.Interface.Internal.Windows.PluginInstaller; using Dalamud.Interface.Internal.Windows.Settings.Widgets; +using Dalamud.Interface.ManagedFontAtlas.Internals; using Dalamud.Interface.Utility; using Dalamud.Utility; using ImGuiNET; @@ -28,7 +30,6 @@ public class SettingsTabLook : SettingsTab }; private float globalUiScale; - private float fontGamma; public override SettingsEntry[] Entries { get; } = { @@ -41,9 +42,8 @@ public class SettingsTabLook : SettingsTab (v, c) => c.UseAxisFontsFromGame = v, v => { - var im = Service.Get(); - im.UseAxisOverride = v; - im.RebuildFonts(); + Service.Get().UseAxisOverride = v; + Service.Get().RebuildFonts(); }), new GapSettingsEntry(5, true), @@ -145,6 +145,7 @@ public class SettingsTabLook : SettingsTab public override void Draw() { var interfaceManager = Service.Get(); + var fontBuildTask = interfaceManager.FontBuildTask; ImGui.AlignTextToFramePadding(); ImGui.Text(Loc.Localize("DalamudSettingsGlobalUiScale", "Global Font Scale")); @@ -164,6 +165,19 @@ public class SettingsTabLook : SettingsTab } } + if (!fontBuildTask.IsCompleted) + { + ImGui.SameLine(); + var buildingFonts = Loc.Localize("DalamudSettingsFontBuildInProgressWithEndingThreeDots", "Building fonts..."); + unsafe + { + var len = Encoding.UTF8.GetByteCount(buildingFonts); + var p = stackalloc byte[len]; + Encoding.UTF8.GetBytes(buildingFonts, new(p, len)); + ImGuiNative.igTextUnformatted(p, (p + len + ((Environment.TickCount / 200) % 3)) - 2); + } + } + var globalUiScaleInPt = 12f * this.globalUiScale; if (ImGui.DragFloat("##DalamudSettingsGlobalUiScaleDrag", ref globalUiScaleInPt, 0.1f, 9.6f, 36f, "%.1fpt", ImGuiSliderFlags.AlwaysClamp)) { @@ -174,33 +188,25 @@ public class SettingsTabLook : SettingsTab ImGuiHelpers.SafeTextColoredWrapped(ImGuiColors.DalamudGrey, Loc.Localize("DalamudSettingsGlobalUiScaleHint", "Scale text in all XIVLauncher UI elements - this is useful for 4K displays.")); - ImGuiHelpers.ScaledDummy(5); - - ImGui.AlignTextToFramePadding(); - ImGui.Text(Loc.Localize("DalamudSettingsFontGamma", "Font Gamma")); - ImGui.SameLine(); - if (ImGui.Button(Loc.Localize("DalamudSettingsIndividualConfigResetToDefaultValue", "Reset") + "##DalamudSettingsFontGammaReset")) + if (fontBuildTask.IsFaulted || fontBuildTask.IsCanceled) { - this.fontGamma = 1.4f; - interfaceManager.FontGammaOverride = this.fontGamma; - interfaceManager.RebuildFonts(); + ImGui.TextColored( + ImGuiColors.DalamudRed, + Loc.Localize("DalamudSettingsFontBuildFaulted", "Failed to load fonts as requested.")); + if (fontBuildTask.Exception is not null + && ImGui.CollapsingHeader("##DalamudSetingsFontBuildFaultReason")) + { + foreach (var e in fontBuildTask.Exception.InnerExceptions) + ImGui.TextUnformatted(e.ToString()); + } } - if (ImGui.DragFloat("##DalamudSettingsFontGammaDrag", ref this.fontGamma, 0.005f, 0.3f, 3f, "%.2f", ImGuiSliderFlags.AlwaysClamp)) - { - interfaceManager.FontGammaOverride = this.fontGamma; - interfaceManager.RebuildFonts(); - } - - ImGuiHelpers.SafeTextColoredWrapped(ImGuiColors.DalamudGrey, Loc.Localize("DalamudSettingsFontGammaHint", "Changes the thickness of text.")); - base.Draw(); } public override void Load() { this.globalUiScale = Service.Get().GlobalUiScale; - this.fontGamma = Service.Get().FontGammaLevel; base.Load(); } diff --git a/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs b/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs index 42bca89ff..9c385a99c 100644 --- a/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs +++ b/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs @@ -7,11 +7,14 @@ using Dalamud.Game; using Dalamud.Game.ClientState; using Dalamud.Game.Gui; using Dalamud.Interface.Animation.EasingFunctions; +using Dalamud.Interface.ManagedFontAtlas; +using Dalamud.Interface.ManagedFontAtlas.Internals; using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; using Dalamud.Interface.Windowing; using Dalamud.Plugin.Services; using Dalamud.Storage.Assets; +using Dalamud.Utility; using ImGuiNET; @@ -27,16 +30,17 @@ internal class TitleScreenMenuWindow : Window, IDisposable private readonly ClientState clientState; private readonly DalamudConfiguration configuration; - private readonly Framework framework; private readonly GameGui gameGui; private readonly TitleScreenMenu titleScreenMenu; + private readonly DisposeSafety.ScopedFinalizer scopedFinalizer = new(); + private readonly IFontAtlas privateAtlas; + private readonly Lazy myFontHandle; private readonly Lazy shadeTexture; private readonly Dictionary shadeEasings = new(); private readonly Dictionary moveEasings = new(); private readonly Dictionary logoEasings = new(); - private readonly Dictionary specialGlyphRequests = new(); private InOutCubic? fadeOutEasing; @@ -48,6 +52,7 @@ internal class TitleScreenMenuWindow : Window, IDisposable /// An instance of . /// An instance of . /// An instance of . + /// An instance of . /// An instance of . /// An instance of . /// An instance of . @@ -55,6 +60,7 @@ internal class TitleScreenMenuWindow : Window, IDisposable ClientState clientState, DalamudConfiguration configuration, DalamudAssetManager dalamudAssetManager, + FontAtlasFactory fontAtlasFactory, Framework framework, GameGui gameGui, TitleScreenMenu titleScreenMenu) @@ -65,7 +71,6 @@ internal class TitleScreenMenuWindow : Window, IDisposable { this.clientState = clientState; this.configuration = configuration; - this.framework = framework; this.gameGui = gameGui; this.titleScreenMenu = titleScreenMenu; @@ -77,9 +82,25 @@ internal class TitleScreenMenuWindow : Window, IDisposable this.PositionCondition = ImGuiCond.Always; this.RespectCloseHotkey = false; + this.shadeTexture = new(() => dalamudAssetManager.GetDalamudTextureWrap(DalamudAsset.TitleScreenMenuShade)); + this.privateAtlas = fontAtlasFactory.CreateFontAtlas(this.WindowName, FontAtlasAutoRebuildMode.Async); + this.scopedFinalizer.Add(this.privateAtlas); + + this.myFontHandle = new( + () => this.scopedFinalizer.Add( + this.privateAtlas.NewDelegateFontHandle( + e => e.OnPreBuild( + toolkit => toolkit.AddDalamudDefaultFont( + TargetFontSizePx, + titleScreenMenu.Entries.SelectMany(x => x.Name).ToGlyphRange()))))); + + titleScreenMenu.EntryListChange += this.TitleScreenMenuEntryListChange; + this.scopedFinalizer.Add(() => titleScreenMenu.EntryListChange -= this.TitleScreenMenuEntryListChange); + this.shadeTexture = new(() => dalamudAssetManager.GetDalamudTextureWrap(DalamudAsset.TitleScreenMenuShade)); framework.Update += this.FrameworkOnUpdate; + this.scopedFinalizer.Add(() => framework.Update -= this.FrameworkOnUpdate); } private enum State @@ -94,6 +115,9 @@ internal class TitleScreenMenuWindow : Window, IDisposable /// public bool AllowDrawing { get; set; } = true; + /// + public void Dispose() => this.scopedFinalizer.Dispose(); + /// public override void PreDraw() { @@ -109,12 +133,6 @@ internal class TitleScreenMenuWindow : Window, IDisposable base.PostDraw(); } - /// - public void Dispose() - { - this.framework.Update -= this.FrameworkOnUpdate; - } - /// public override void Draw() { @@ -246,33 +264,12 @@ internal class TitleScreenMenuWindow : Window, IDisposable break; } } - - var srcText = entries.Select(e => e.Name).ToHashSet(); - var keys = this.specialGlyphRequests.Keys.ToHashSet(); - keys.RemoveWhere(x => srcText.Contains(x)); - foreach (var key in keys) - { - this.specialGlyphRequests[key].Dispose(); - this.specialGlyphRequests.Remove(key); - } } private bool DrawEntry( TitleScreenMenuEntry entry, bool inhibitFadeout, bool showText, bool isFirst, bool overrideAlpha, bool interactable) { - InterfaceManager.SpecialGlyphRequest fontHandle; - if (this.specialGlyphRequests.TryGetValue(entry.Name, out fontHandle) && fontHandle.Size != TargetFontSizePx) - { - fontHandle.Dispose(); - this.specialGlyphRequests.Remove(entry.Name); - fontHandle = null; - } - - if (fontHandle == null) - this.specialGlyphRequests[entry.Name] = fontHandle = Service.Get().NewFontSizeRef(TargetFontSizePx, entry.Name); - - ImGui.PushFont(fontHandle.Font); - ImGui.SetWindowFontScale(TargetFontSizePx / fontHandle.Size); + using var fontScopeDispose = this.myFontHandle.Value.Push(); var scale = ImGui.GetIO().FontGlobalScale; @@ -383,8 +380,6 @@ internal class TitleScreenMenuWindow : Window, IDisposable initialCursor.Y += entry.Texture.Height * scale; ImGui.SetCursorPos(initialCursor); - ImGui.PopFont(); - return isHover; } @@ -401,4 +396,6 @@ internal class TitleScreenMenuWindow : Window, IDisposable if (charaMake != IntPtr.Zero || charaSelect != IntPtr.Zero || titleDcWorldMap != IntPtr.Zero) this.IsOpen = false; } + + private void TitleScreenMenuEntryListChange() => this.privateAtlas.BuildFontsAsync(); } diff --git a/Dalamud/Interface/ManagedFontAtlas/FontAtlasAutoRebuildMode.cs b/Dalamud/Interface/ManagedFontAtlas/FontAtlasAutoRebuildMode.cs new file mode 100644 index 000000000..50e591390 --- /dev/null +++ b/Dalamud/Interface/ManagedFontAtlas/FontAtlasAutoRebuildMode.cs @@ -0,0 +1,22 @@ +namespace Dalamud.Interface.ManagedFontAtlas; + +/// +/// How to rebuild . +/// +public enum FontAtlasAutoRebuildMode +{ + /// + /// Do not rebuild. + /// + Disable, + + /// + /// Rebuild on new frame. + /// + OnNewFrame, + + /// + /// Rebuild asynchronously. + /// + Async, +} diff --git a/Dalamud/Interface/ManagedFontAtlas/FontAtlasBuildStep.cs b/Dalamud/Interface/ManagedFontAtlas/FontAtlasBuildStep.cs new file mode 100644 index 000000000..345ab729d --- /dev/null +++ b/Dalamud/Interface/ManagedFontAtlas/FontAtlasBuildStep.cs @@ -0,0 +1,38 @@ +using ImGuiNET; + +namespace Dalamud.Interface.ManagedFontAtlas; + +/// +/// Build step for . +/// +public enum FontAtlasBuildStep +{ + /// + /// An invalid value. This should never be passed through event callbacks. + /// + Invalid, + + /// + /// Called before calling .
+ /// Expect to be passed. + ///
+ PreBuild, + + /// + /// Called after calling .
+ /// Expect to be passed.
+ ///
+ /// This callback is not guaranteed to happen after , + /// but it will never happen on its own. + ///
+ PostBuild, + + /// + /// Called after promoting staging font atlas to the actual atlas for .
+ /// Expect to be passed.
+ ///
+ /// This callback is not guaranteed to happen after , + /// but it will never happen on its own. + ///
+ PostPromotion, +} diff --git a/Dalamud/Interface/ManagedFontAtlas/FontAtlasBuildStepDelegate.cs b/Dalamud/Interface/ManagedFontAtlas/FontAtlasBuildStepDelegate.cs new file mode 100644 index 000000000..4f5b34061 --- /dev/null +++ b/Dalamud/Interface/ManagedFontAtlas/FontAtlasBuildStepDelegate.cs @@ -0,0 +1,15 @@ +namespace Dalamud.Interface.ManagedFontAtlas; + +/// +/// Delegate to be called when a font needs to be built. +/// +/// A toolkit that may help you for font building steps. +/// +/// An implementation of may implement all of +/// , , and +/// .
+/// Either use to identify the build step, or use +/// , , +/// and for routing. +///
+public delegate void FontAtlasBuildStepDelegate(IFontAtlasBuildToolkit toolkit); diff --git a/Dalamud/Interface/ManagedFontAtlas/FontAtlasBuildToolkitUtilities.cs b/Dalamud/Interface/ManagedFontAtlas/FontAtlasBuildToolkitUtilities.cs new file mode 100644 index 000000000..586887a3b --- /dev/null +++ b/Dalamud/Interface/ManagedFontAtlas/FontAtlasBuildToolkitUtilities.cs @@ -0,0 +1,133 @@ +using System.Collections.Generic; +using System.Runtime.CompilerServices; + +using Dalamud.Interface.Utility; + +using ImGuiNET; + +namespace Dalamud.Interface.ManagedFontAtlas; + +/// +/// Convenience function for building fonts through . +/// +public static class FontAtlasBuildToolkitUtilities +{ + /// + /// Compiles given s into an array of containing ImGui glyph ranges. + /// + /// The chars. + /// Add fallback codepoints to the range. + /// Add ellipsis codepoints to the range. + /// The compiled range. + public static ushort[] ToGlyphRange( + this IEnumerable enumerable, + bool addFallbackCodepoints = true, + bool addEllipsisCodepoints = true) + { + using var builderScoped = ImGuiHelpers.NewFontGlyphRangeBuilderPtrScoped(out var builder); + foreach (var c in enumerable) + builder.AddChar(c); + return builder.BuildRangesToArray(addFallbackCodepoints, addEllipsisCodepoints); + } + + /// + /// Compiles given s into an array of containing ImGui glyph ranges. + /// + /// The chars. + /// Add fallback codepoints to the range. + /// Add ellipsis codepoints to the range. + /// The compiled range. + public static ushort[] ToGlyphRange( + this ReadOnlySpan span, + bool addFallbackCodepoints = true, + bool addEllipsisCodepoints = true) + { + using var builderScoped = ImGuiHelpers.NewFontGlyphRangeBuilderPtrScoped(out var builder); + foreach (var c in span) + builder.AddChar(c); + return builder.BuildRangesToArray(addFallbackCodepoints, addEllipsisCodepoints); + } + + /// + /// Compiles given string into an array of containing ImGui glyph ranges. + /// + /// The string. + /// Add fallback codepoints to the range. + /// Add ellipsis codepoints to the range. + /// The compiled range. + public static ushort[] ToGlyphRange( + this string @string, + bool addFallbackCodepoints = true, + bool addEllipsisCodepoints = true) => + @string.AsSpan().ToGlyphRange(addFallbackCodepoints, addEllipsisCodepoints); + + /// + /// Finds the corresponding in + /// . that corresponds to the + /// specified font . + /// + /// The toolkit. + /// The font. + /// The relevant config pointer, or empty config pointer if not found. + public static unsafe ImFontConfigPtr FindConfigPtr(this IFontAtlasBuildToolkit toolkit, ImFontPtr fontPtr) + { + foreach (ref var c in toolkit.NewImAtlas.ConfigDataWrapped().DataSpan) + { + if (c.DstFont == fontPtr.NativePtr) + return new((nint)Unsafe.AsPointer(ref c)); + } + + return default; + } + + /// + /// Invokes + /// if of + /// is . + /// + /// The toolkit. + /// The action. + /// This, for method chaining. + public static IFontAtlasBuildToolkit OnPreBuild( + this IFontAtlasBuildToolkit toolkit, + Action action) + { + if (toolkit.BuildStep is FontAtlasBuildStep.PreBuild) + action.Invoke((IFontAtlasBuildToolkitPreBuild)toolkit); + return toolkit; + } + + /// + /// Invokes + /// if of + /// is . + /// + /// The toolkit. + /// The action. + /// toolkit, for method chaining. + public static IFontAtlasBuildToolkit OnPostBuild( + this IFontAtlasBuildToolkit toolkit, + Action action) + { + if (toolkit.BuildStep is FontAtlasBuildStep.PostBuild) + action.Invoke((IFontAtlasBuildToolkitPostBuild)toolkit); + return toolkit; + } + + /// + /// Invokes + /// if of + /// is . + /// + /// The toolkit. + /// The action. + /// toolkit, for method chaining. + public static IFontAtlasBuildToolkit OnPostPromotion( + this IFontAtlasBuildToolkit toolkit, + Action action) + { + if (toolkit.BuildStep is FontAtlasBuildStep.PostPromotion) + action.Invoke((IFontAtlasBuildToolkitPostPromotion)toolkit); + return toolkit; + } +} diff --git a/Dalamud/Interface/ManagedFontAtlas/IFontAtlas.cs b/Dalamud/Interface/ManagedFontAtlas/IFontAtlas.cs new file mode 100644 index 000000000..ec3e66e9a --- /dev/null +++ b/Dalamud/Interface/ManagedFontAtlas/IFontAtlas.cs @@ -0,0 +1,141 @@ +using System.Threading.Tasks; + +using Dalamud.Interface.GameFonts; +using Dalamud.Interface.Utility; + +using ImGuiNET; + +namespace Dalamud.Interface.ManagedFontAtlas; + +/// +/// Wrapper for . +/// +public interface IFontAtlas : IDisposable +{ + /// + /// Event to be called on build step changes.
+ /// is meaningless for this event. + ///
+ event FontAtlasBuildStepDelegate? BuildStepChange; + + /// + /// Event fired when a font rebuild operation is recommended.
+ /// This event will be invoked from the main thread.
+ ///
+ /// Reasons for the event include changes in and + /// initialization of new associated font handles. + ///
+ /// + /// You should call or + /// if is not set to true.
+ /// Avoid calling here; it will block the main thread. + ///
+ event Action? RebuildRecommend; + + /// + /// Gets the name of the atlas. For logging and debugging purposes. + /// + string Name { get; } + + /// + /// Gets a value how the atlas should be rebuilt when the relevant Dalamud Configuration changes. + /// + FontAtlasAutoRebuildMode AutoRebuildMode { get; } + + /// + /// Gets the font atlas. Might be empty. + /// + ImFontAtlasPtr ImAtlas { get; } + + /// + /// Gets the task that represents the current font rebuild state. + /// + Task BuildTask { get; } + + /// + /// Gets a value indicating whether there exists any built atlas, regardless of . + /// + bool HasBuiltAtlas { get; } + + /// + /// Gets a value indicating whether this font atlas is under the effect of global scale. + /// + bool IsGlobalScaled { get; } + + /// + /// Suppresses automatically rebuilding fonts for the scope. + /// + /// An instance of that will release the suppression. + /// + /// Use when you will be creating multiple new handles, and want rebuild to trigger only when you're done doing so. + /// This function will effectively do nothing, if is set to + /// . + /// + /// + /// + /// using (atlas.SuppressBuild()) { + /// this.font1 = atlas.NewGameFontHandle(...); + /// this.font2 = atlas.NewDelegateFontHandle(...); + /// } + /// + /// + public IDisposable SuppressAutoRebuild(); + + /// + /// Creates a new from game's built-in fonts. + /// + /// Font to use. + /// Handle to a font that may or may not be ready yet. + public IFontHandle NewGameFontHandle(GameFontStyle style); + + /// + /// Creates a new IFontHandle using your own callbacks. + /// + /// Callback for . + /// Handle to a font that may or may not be ready yet. + /// + /// On initialization: + /// + /// this.fontHandle = atlas.NewDelegateFontHandle(e => e.OnPreBuild(tk => { + /// var config = new SafeFontConfig { SizePx = 16 }; + /// config.MergeFont = tk.AddFontFromFile(@"C:\Windows\Fonts\comic.ttf", config); + /// tk.AddGameSymbol(config); + /// tk.AddExtraGlyphsForDalamudLanguage(config); + /// // optionally do the following if you have to add more than one font here, + /// // to specify which font added during this delegate is the final font to use. + /// tk.Font = config.MergeFont; + /// })); + /// // or + /// this.fontHandle = atlas.NewDelegateFontHandle(e => e.OnPreBuild(tk => tk.AddDalamudDefaultFont(36))); + /// + ///
+ /// On use: + /// + /// using (this.fontHandle.Push()) + /// ImGui.TextUnformatted("Example"); + /// + ///
+ public IFontHandle NewDelegateFontHandle(FontAtlasBuildStepDelegate buildStepDelegate); + + /// + /// Queues rebuilding fonts, on the main thread.
+ /// Note that would not necessarily get changed from calling this function. + ///
+ /// If is . + void BuildFontsOnNextFrame(); + + /// + /// Rebuilds fonts immediately, on the current thread.
+ /// Even the callback for will be called on the same thread. + ///
+ /// If is . + void BuildFontsImmediately(); + + /// + /// Rebuilds fonts asynchronously, on any thread. + /// + /// Call on the main thread. + /// The task. + /// If is . + Task BuildFontsAsync(bool callPostPromotionOnMainThread = true); +} diff --git a/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkit.cs b/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkit.cs new file mode 100644 index 000000000..4b016bbb2 --- /dev/null +++ b/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkit.cs @@ -0,0 +1,67 @@ +using System.Runtime.InteropServices; + +using Dalamud.Interface.Utility; + +using ImGuiNET; + +namespace Dalamud.Interface.ManagedFontAtlas; + +/// +/// Common stuff for and . +/// +public interface IFontAtlasBuildToolkit +{ + /// + /// Gets or sets the font relevant to the call. + /// + ImFontPtr Font { get; set; } + + /// + /// Gets the current scale this font atlas is being built with. + /// + float Scale { get; } + + /// + /// Gets a value indicating whether the current build operation is asynchronous. + /// + bool IsAsyncBuildOperation { get; } + + /// + /// Gets the current build step. + /// + FontAtlasBuildStep BuildStep { get; } + + /// + /// Gets the font atlas being built. + /// + ImFontAtlasPtr NewImAtlas { get; } + + /// + /// Gets the wrapper for of .
+ /// This does not need to be disposed. Calling does nothing.- + ///
+ /// Modification of this vector may result in undefined behaviors. + ///
+ ImVectorWrapper Fonts { get; } + + /// + /// Queues an item to be disposed after the native atlas gets disposed, successful or not. + /// + /// Disposable type. + /// The disposable. + /// The same . + T DisposeWithAtlas(T disposable) where T : IDisposable; + + /// + /// Queues an item to be disposed after the native atlas gets disposed, successful or not. + /// + /// The gc handle. + /// The same . + GCHandle DisposeWithAtlas(GCHandle gcHandle); + + /// + /// Queues an item to be disposed after the native atlas gets disposed, successful or not. + /// + /// The action to run on dispose. + void DisposeWithAtlas(Action action); +} diff --git a/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPostBuild.cs b/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPostBuild.cs new file mode 100644 index 000000000..3c14197e0 --- /dev/null +++ b/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPostBuild.cs @@ -0,0 +1,26 @@ +using Dalamud.Interface.Internal; + +using ImGuiNET; + +namespace Dalamud.Interface.ManagedFontAtlas; + +/// +/// Toolkit for use when the build state is . +/// +public interface IFontAtlasBuildToolkitPostBuild : IFontAtlasBuildToolkit +{ + /// + /// Gets whether global scaling is ignored for the given font. + /// + /// The font. + /// True if ignored. + bool IsGlobalScaleIgnored(ImFontPtr fontPtr); + + /// + /// Stores a texture to be managed with the atlas. + /// + /// The texture wrap. + /// Dispose the wrap on error. + /// The texture index. + int StoreTexture(IDalamudTextureWrap textureWrap, bool disposeOnError); +} diff --git a/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPostPromotion.cs b/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPostPromotion.cs new file mode 100644 index 000000000..8c3c91624 --- /dev/null +++ b/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPostPromotion.cs @@ -0,0 +1,33 @@ +using ImGuiNET; + +namespace Dalamud.Interface.ManagedFontAtlas; + +/// +/// Toolkit for use when the build state is . +/// +public interface IFontAtlasBuildToolkitPostPromotion : IFontAtlasBuildToolkit +{ + /// + /// Copies glyphs across fonts, in a safer way.
+ /// If the font does not belong to the current atlas, this function is a no-op. + ///
+ /// Source font. + /// Target font. + /// Whether to copy missing glyphs only. + /// Whether to call target.BuildLookupTable(). + /// Low codepoint range to copy. + /// High codepoing range to copy. + void CopyGlyphsAcrossFonts( + ImFontPtr source, + ImFontPtr target, + bool missingOnly, + bool rebuildLookupTable = true, + char rangeLow = ' ', + char rangeHigh = '\uFFFE'); + + /// + /// Calls , with some fixups. + /// + /// The font. + void BuildLookupTable(ImFontPtr font); +} diff --git a/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPreBuild.cs b/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPreBuild.cs new file mode 100644 index 000000000..cb8a27a54 --- /dev/null +++ b/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPreBuild.cs @@ -0,0 +1,186 @@ +using System.IO; +using System.Runtime.InteropServices; + +using Dalamud.Interface.GameFonts; +using Dalamud.Interface.Utility; + +using ImGuiNET; + +namespace Dalamud.Interface.ManagedFontAtlas; + +/// +/// Toolkit for use when the build state is .
+///
+/// After returns, +/// either must be set, +/// or at least one font must have been added to the atlas using one of AddFont... functions. +///
+public interface IFontAtlasBuildToolkitPreBuild : IFontAtlasBuildToolkit +{ + /// + /// Queues an item to be disposed after the whole build process gets complete, successful or not. + /// + /// Disposable type. + /// The disposable. + /// The same . + T DisposeAfterBuild(T disposable) where T : IDisposable; + + /// + /// Queues an item to be disposed after the whole build process gets complete, successful or not. + /// + /// The gc handle. + /// The same . + GCHandle DisposeAfterBuild(GCHandle gcHandle); + + /// + /// Queues an item to be disposed after the whole build process gets complete, successful or not. + /// + /// The action to run on dispose. + void DisposeAfterBuild(Action action); + + /// + /// Excludes given font from global scaling. + /// + /// The font. + /// Same with . + ImFontPtr IgnoreGlobalScale(ImFontPtr fontPtr); + + /// + /// Gets whether global scaling is ignored for the given font. + /// + /// The font. + /// True if ignored. + bool IsGlobalScaleIgnored(ImFontPtr fontPtr); + + /// + /// Adds a font from memory region allocated using .
+ /// It WILL crash if you try to use a memory pointer allocated in some other way.
+ /// + /// Do NOT call on the once this function has + /// been called, unless is set and the function has thrown an error. + /// + ///
+ /// Memory address for the data allocated using . + /// The size of the font file.. + /// The font config. + /// Free if an exception happens. + /// A debug tag. + /// The newly added font. + unsafe ImFontPtr AddFontFromImGuiHeapAllocatedMemory( + nint dataPointer, + int dataSize, + in SafeFontConfig fontConfig, + bool freeOnException, + string debugTag) + => this.AddFontFromImGuiHeapAllocatedMemory( + (void*)dataPointer, + dataSize, + fontConfig, + freeOnException, + debugTag); + + /// + /// Adds a font from memory region allocated using .
+ /// It WILL crash if you try to use a memory pointer allocated in some other way.
+ /// Do NOT call on the once this + /// function has been called. + ///
+ /// Memory address for the data allocated using . + /// The size of the font file.. + /// The font config. + /// Free if an exception happens. + /// A debug tag. + /// The newly added font. + unsafe ImFontPtr AddFontFromImGuiHeapAllocatedMemory( + void* dataPointer, + int dataSize, + in SafeFontConfig fontConfig, + bool freeOnException, + string debugTag); + + /// + /// Adds a font from a file. + /// + /// The file path to create a new font from. + /// The font config. + /// The newly added font. + ImFontPtr AddFontFromFile(string path, in SafeFontConfig fontConfig); + + /// + /// Adds a font from a stream. + /// + /// The stream to create a new font from. + /// The font config. + /// Dispose when this function returns or throws. + /// A debug tag. + /// The newly added font. + ImFontPtr AddFontFromStream(Stream stream, in SafeFontConfig fontConfig, bool leaveOpen, string debugTag); + + /// + /// Adds a font from memory. + /// + /// The span to create from. + /// The font config. + /// A debug tag. + /// The newly added font. + ImFontPtr AddFontFromMemory(ReadOnlySpan span, in SafeFontConfig fontConfig, string debugTag); + + /// + /// Adds the default font known to the current font atlas.
+ ///
+ /// Includes and .
+ /// As this involves adding multiple fonts, calling this function will set + /// as the return value of this function, if it was empty before. + ///
+ /// Font size in pixels. + /// The glyph ranges. Use .ToGlyphRange to build. + /// A font returned from . + ImFontPtr AddDalamudDefaultFont(float sizePx, ushort[]? glyphRanges = null); + + /// + /// Adds a font that is shipped with Dalamud.
+ ///
+ /// Note: if game symbols font file is requested but is unavailable, + /// then it will take the glyphs from game's built-in fonts, and everything in + /// will be ignored but , , + /// and . + ///
+ /// The font type. + /// The font config. + /// The added font. + ImFontPtr AddDalamudAssetFont(DalamudAsset asset, in SafeFontConfig fontConfig); + + /// + /// Same with (, ...), + /// but using only FontAwesome icon ranges.
+ /// will be ignored. + ///
+ /// The font config. + /// The added font. + ImFontPtr AddFontAwesomeIconFont(in SafeFontConfig fontConfig); + + /// + /// Adds the game's symbols into the provided font.
+ /// will be ignored.
+ /// If the game symbol font file is unavailable, only will be honored. + ///
+ /// The font config. + /// The added font. + ImFontPtr AddGameSymbol(in SafeFontConfig fontConfig); + + /// + /// Adds the game glyphs to the font. + /// + /// The font style. + /// The glyph ranges. + /// The font to merge to. If empty, then a new font will be created. + /// The added font. + ImFontPtr AddGameGlyphs(GameFontStyle gameFontStyle, ushort[]? glyphRanges, ImFontPtr mergeFont); + + /// + /// Adds glyphs of extra languages into the provided font, depending on Dalamud Configuration.
+ /// will be ignored. + ///
+ /// The font config. + void AttachExtraGlyphsForDalamudLanguage(in SafeFontConfig fontConfig); +} diff --git a/Dalamud/Interface/ManagedFontAtlas/IFontHandle.cs b/Dalamud/Interface/ManagedFontAtlas/IFontHandle.cs new file mode 100644 index 000000000..854594663 --- /dev/null +++ b/Dalamud/Interface/ManagedFontAtlas/IFontHandle.cs @@ -0,0 +1,42 @@ +using ImGuiNET; + +namespace Dalamud.Interface.ManagedFontAtlas; + +/// +/// Represents a reference counting handle for fonts. +/// +public interface IFontHandle : IDisposable +{ + /// + /// Represents a reference counting handle for fonts. Dalamud internal use only. + /// + internal interface IInternal : IFontHandle + { + /// + /// Gets the font.
+ /// Use of this properly is safe only from the UI thread.
+ /// Use if the intended purpose of this property is .
+ /// Futures changes may make simple not enough. + ///
+ ImFontPtr ImFont { get; } + } + + /// + /// Gets the load exception, if it failed to load. Otherwise, it is null. + /// + Exception? LoadException { get; } + + /// + /// Gets a value indicating whether this font is ready for use.
+ /// Use directly if you want to keep the current ImGui font if the font is not ready. + ///
+ bool Available { get; } + + /// + /// Pushes the current font into ImGui font stack using , if available.
+ /// Use to access the current font.
+ /// You may not access the font once you dispose this object. + ///
+ /// A disposable object that will call (1) on dispose. + IDisposable Push(); +} diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/DelegateFontHandle.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/DelegateFontHandle.cs new file mode 100644 index 000000000..f0ed09155 --- /dev/null +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/DelegateFontHandle.cs @@ -0,0 +1,334 @@ +using System.Collections.Generic; +using System.Linq; + +using Dalamud.Interface.Utility; +using Dalamud.Interface.Utility.Raii; +using Dalamud.Logging.Internal; + +using ImGuiNET; + +namespace Dalamud.Interface.ManagedFontAtlas.Internals; + +/// +/// A font handle representing a user-callback generated font. +/// +internal class DelegateFontHandle : IFontHandle.IInternal +{ + private IFontHandleManager? manager; + + /// + /// Initializes a new instance of the class. + /// + /// An instance of . + /// Callback for . + public DelegateFontHandle(IFontHandleManager manager, FontAtlasBuildStepDelegate callOnBuildStepChange) + { + this.manager = manager; + this.CallOnBuildStepChange = callOnBuildStepChange; + } + + /// + /// Gets the function to be called on build step changes. + /// + public FontAtlasBuildStepDelegate CallOnBuildStepChange { get; } + + /// + public Exception? LoadException => this.ManagerNotDisposed.Substance?.GetBuildException(this); + + /// + public bool Available => this.ImFont.IsNotNullAndLoaded(); + + /// + public ImFontPtr ImFont => this.ManagerNotDisposed.Substance?.GetFontPtr(this) ?? default; + + private IFontHandleManager ManagerNotDisposed => + this.manager ?? throw new ObjectDisposedException(nameof(GamePrebakedFontHandle)); + + /// + public void Dispose() + { + this.manager?.FreeFontHandle(this); + this.manager = null; + } + + /// + public IDisposable Push() => ImRaii.PushFont(this.ImFont, this.Available); + + /// + /// Manager for s. + /// + internal sealed class HandleManager : IFontHandleManager + { + private readonly HashSet handles = new(); + private readonly object syncRoot = new(); + + /// + /// Initializes a new instance of the class. + /// + /// The name of the owner atlas. + public HandleManager(string atlasName) => this.Name = $"{atlasName}:{nameof(DelegateFontHandle)}:Manager"; + + /// + public event Action? RebuildRecommend; + + /// + public string Name { get; } + + /// + public IFontHandleSubstance? Substance { get; set; } + + /// + public void Dispose() + { + lock (this.syncRoot) + { + this.handles.Clear(); + this.Substance?.Dispose(); + this.Substance = null; + } + } + + /// + public IFontHandle NewFontHandle(FontAtlasBuildStepDelegate buildStepDelegate) + { + var key = new DelegateFontHandle(this, buildStepDelegate); + lock (this.syncRoot) + this.handles.Add(key); + this.RebuildRecommend?.Invoke(); + return key; + } + + /// + public void FreeFontHandle(IFontHandle handle) + { + if (handle is not DelegateFontHandle cgfh) + return; + + lock (this.syncRoot) + this.handles.Remove(cgfh); + } + + /// + public IFontHandleSubstance NewSubstance() + { + lock (this.syncRoot) + return new HandleSubstance(this, this.handles.ToArray()); + } + } + + /// + /// Substance from . + /// + internal sealed class HandleSubstance : IFontHandleSubstance + { + private static readonly ModuleLog Log = new($"{nameof(DelegateFontHandle)}.{nameof(HandleSubstance)}"); + + // Not owned by this class. Do not dispose. + private readonly DelegateFontHandle[] relevantHandles; + + // Owned by this class, but ImFontPtr values still do not belong to this. + private readonly Dictionary fonts = new(); + private readonly Dictionary buildExceptions = new(); + + /// + /// Initializes a new instance of the class. + /// + /// The manager. + /// The relevant handles. + public HandleSubstance(IFontHandleManager manager, DelegateFontHandle[] relevantHandles) + { + this.Manager = manager; + this.relevantHandles = relevantHandles; + } + + /// + public IFontHandleManager Manager { get; } + + /// + public void Dispose() + { + this.fonts.Clear(); + this.buildExceptions.Clear(); + } + + /// + public ImFontPtr GetFontPtr(IFontHandle handle) => + handle is DelegateFontHandle cgfh ? this.fonts.GetValueOrDefault(cgfh) : default; + + /// + public Exception? GetBuildException(IFontHandle handle) => + handle is DelegateFontHandle cgfh ? this.buildExceptions.GetValueOrDefault(cgfh) : default; + + /// + public void OnPreBuild(IFontAtlasBuildToolkitPreBuild toolkitPreBuild) + { + var fontsVector = toolkitPreBuild.Fonts; + foreach (var k in this.relevantHandles) + { + var fontCountPrevious = fontsVector.Length; + + try + { + toolkitPreBuild.Font = default; + k.CallOnBuildStepChange(toolkitPreBuild); + if (toolkitPreBuild.Font.IsNull()) + { + if (fontCountPrevious == fontsVector.Length) + { + throw new InvalidOperationException( + $"{nameof(FontAtlasBuildStepDelegate)} must either set the " + + $"{nameof(IFontAtlasBuildToolkitPreBuild.Font)} property, or add at least one font."); + } + + toolkitPreBuild.Font = fontsVector[^1]; + } + else + { + var found = false; + unsafe + { + for (var i = fontCountPrevious; !found && i < fontsVector.Length; i++) + { + if (fontsVector[i].NativePtr == toolkitPreBuild.Font.NativePtr) + found = true; + } + } + + if (!found) + { + throw new InvalidOperationException( + "The font does not exist in the atlas' font array. If you need an empty font, try" + + "adding Noto Sans from Dalamud Assets, but using new ushort[]{ ' ', ' ', 0 } as the" + + "glyph range."); + } + } + + if (fontsVector.Length - fontCountPrevious != 1) + { + Log.Warning( + "[{name}:Substance] {n} fonts added from {delegate} PreBuild call; " + + "Using the most recently added font. " + + "Did you mean to use {sfd}.{sfdprop} or {ifcp}.{ifcpprop}?", + this.Manager.Name, + fontsVector.Length - fontCountPrevious, + nameof(FontAtlasBuildStepDelegate), + nameof(SafeFontConfig), + nameof(SafeFontConfig.MergeFont), + nameof(ImFontConfigPtr), + nameof(ImFontConfigPtr.MergeMode)); + } + + for (var i = fontCountPrevious; i < fontsVector.Length; i++) + { + if (fontsVector[i].ValidateUnsafe() is { } ex) + { + throw new InvalidOperationException( + "One of the newly added fonts seem to be pointing to an invalid memory address.", + ex); + } + } + + // Check for duplicate entries; duplicates will result in free-after-free + for (var i = 0; i < fontCountPrevious; i++) + { + for (var j = fontCountPrevious; j < fontsVector.Length; j++) + { + unsafe + { + if (fontsVector[i].NativePtr == fontsVector[j].NativePtr) + throw new InvalidOperationException("An already added font has been added again."); + } + } + } + + this.fonts[k] = toolkitPreBuild.Font; + } + catch (Exception e) + { + this.fonts[k] = default; + this.buildExceptions[k] = e; + + Log.Error( + e, + "[{name}:Substance] An error has occurred while during {delegate} PreBuild call.", + this.Manager.Name, + nameof(FontAtlasBuildStepDelegate)); + + // Sanitization, in a futile attempt to prevent crashes on invalid parameters + unsafe + { + var distinct = + fontsVector + .DistinctBy(x => (nint)x.NativePtr) // Remove duplicates + .Where(x => x.ValidateUnsafe() is null) // Remove invalid entries without freeing them + .ToArray(); + + // We're adding the contents back; do not destroy the contents + fontsVector.Clear(true); + fontsVector.AddRange(distinct.AsSpan()); + } + } + } + } + + /// + public void OnPreBuildCleanup(IFontAtlasBuildToolkitPreBuild toolkitPreBuild) + { + // irrelevant + } + + /// + public void OnPostBuild(IFontAtlasBuildToolkitPostBuild toolkitPostBuild) + { + foreach (var k in this.relevantHandles) + { + if (!this.fonts[k].IsNotNullAndLoaded()) + continue; + + try + { + toolkitPostBuild.Font = this.fonts[k]; + k.CallOnBuildStepChange.Invoke(toolkitPostBuild); + } + catch (Exception e) + { + this.fonts[k] = default; + this.buildExceptions[k] = e; + + Log.Error( + e, + "[{name}] An error has occurred while during {delegate} PostBuild call.", + this.Manager.Name, + nameof(FontAtlasBuildStepDelegate)); + } + } + } + + /// + public void OnPostPromotion(IFontAtlasBuildToolkitPostPromotion toolkitPostPromotion) + { + foreach (var k in this.relevantHandles) + { + if (!this.fonts[k].IsNotNullAndLoaded()) + continue; + + try + { + toolkitPostPromotion.Font = this.fonts[k]; + k.CallOnBuildStepChange.Invoke(toolkitPostPromotion); + } + catch (Exception e) + { + this.fonts[k] = default; + this.buildExceptions[k] = e; + + Log.Error( + e, + "[{name}:Substance] An error has occurred while during {delegate} PostPromotion call.", + this.Manager.Name, + nameof(FontAtlasBuildStepDelegate)); + } + } + } + } +} diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs new file mode 100644 index 000000000..e73ea7548 --- /dev/null +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs @@ -0,0 +1,682 @@ +using System.Buffers; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text.Unicode; + +using Dalamud.Configuration.Internal; +using Dalamud.Interface.GameFonts; +using Dalamud.Interface.Internal; +using Dalamud.Interface.Utility; +using Dalamud.Storage.Assets; +using Dalamud.Utility; + +using ImGuiNET; + +using SharpDX.DXGI; + +namespace Dalamud.Interface.ManagedFontAtlas.Internals; + +/// +/// Standalone font atlas. +/// +internal sealed partial class FontAtlasFactory +{ + private static readonly Dictionary> PairAdjustmentsCache = + new(); + + /// + /// Implementations for and + /// . + /// + private class BuildToolkit : IFontAtlasBuildToolkitPreBuild, IFontAtlasBuildToolkitPostBuild, IDisposable + { + private static readonly ushort FontAwesomeIconMin = + (ushort)Enum.GetValues().Where(x => x > 0).Min(); + + private static readonly ushort FontAwesomeIconMax = + (ushort)Enum.GetValues().Where(x => x > 0).Max(); + + private readonly DisposeSafety.ScopedFinalizer disposeAfterBuild = new(); + private readonly GamePrebakedFontHandle.HandleSubstance gameFontHandleSubstance; + private readonly FontAtlasFactory factory; + private readonly FontAtlasBuiltData data; + + /// + /// Initializes a new instance of the class. + /// + /// An instance of . + /// New atlas. + /// An instance of . + /// Specify whether the current build operation is an asynchronous one. + public BuildToolkit( + FontAtlasFactory factory, + FontAtlasBuiltData data, + GamePrebakedFontHandle.HandleSubstance gameFontHandleSubstance, + bool isAsync) + { + this.data = data; + this.gameFontHandleSubstance = gameFontHandleSubstance; + this.IsAsyncBuildOperation = isAsync; + this.factory = factory; + } + + /// + public ImFontPtr Font { get; set; } + + /// + public float Scale => this.data.Scale; + + /// + public bool IsAsyncBuildOperation { get; } + + /// + public FontAtlasBuildStep BuildStep { get; set; } + + /// + public ImFontAtlasPtr NewImAtlas => this.data.Atlas; + + /// + public ImVectorWrapper Fonts => this.data.Fonts; + + /// + /// Gets the list of fonts to ignore global scale. + /// + public List GlobalScaleExclusions { get; } = new(); + + /// + public void Dispose() => this.disposeAfterBuild.Dispose(); + + /// + public T2 DisposeAfterBuild(T2 disposable) where T2 : IDisposable => + this.disposeAfterBuild.Add(disposable); + + /// + public GCHandle DisposeAfterBuild(GCHandle gcHandle) => this.disposeAfterBuild.Add(gcHandle); + + /// + public void DisposeAfterBuild(Action action) => this.disposeAfterBuild.Add(action); + + /// + public T DisposeWithAtlas(T disposable) where T : IDisposable => this.data.Garbage.Add(disposable); + + /// + public GCHandle DisposeWithAtlas(GCHandle gcHandle) => this.data.Garbage.Add(gcHandle); + + /// + public void DisposeWithAtlas(Action action) => this.data.Garbage.Add(action); + + /// + public ImFontPtr IgnoreGlobalScale(ImFontPtr fontPtr) + { + this.GlobalScaleExclusions.Add(fontPtr); + return fontPtr; + } + + /// + public bool IsGlobalScaleIgnored(ImFontPtr fontPtr) => + this.GlobalScaleExclusions.Contains(fontPtr); + + /// + public int StoreTexture(IDalamudTextureWrap textureWrap, bool disposeOnError) => + this.data.AddNewTexture(textureWrap, disposeOnError); + + /// + public unsafe ImFontPtr AddFontFromImGuiHeapAllocatedMemory( + void* dataPointer, + int dataSize, + in SafeFontConfig fontConfig, + bool freeOnException, + string debugTag) + { + Log.Verbose( + "[{name}] 0x{atlas:X}: {funcname}(0x{dataPointer:X}, 0x{dataSize:X}, ...) from {tag}", + this.data.Owner?.Name ?? "(error)", + (nint)this.NewImAtlas.NativePtr, + nameof(this.AddFontFromImGuiHeapAllocatedMemory), + (nint)dataPointer, + dataSize, + debugTag); + + try + { + fontConfig.ThrowOnInvalidValues(); + + var raw = fontConfig.Raw with + { + FontData = dataPointer, + FontDataSize = dataSize, + }; + + if (fontConfig.GlyphRanges is not { Length: > 0 } ranges) + ranges = new ushort[] { 1, 0xFFFE, 0 }; + + raw.GlyphRanges = (ushort*)this.DisposeAfterBuild( + GCHandle.Alloc(ranges, GCHandleType.Pinned)).AddrOfPinnedObject(); + + TrueTypeUtils.CheckImGuiCompatibleOrThrow(raw); + + var font = this.NewImAtlas.AddFont(&raw); + + var dataHash = default(HashCode); + dataHash.AddBytes(new(dataPointer, dataSize)); + var hashIdent = (uint)dataHash.ToHashCode() | ((ulong)dataSize << 32); + + List<(char Left, char Right, float Distance)> pairAdjustments; + lock (PairAdjustmentsCache) + { + if (!PairAdjustmentsCache.TryGetValue(hashIdent, out pairAdjustments)) + { + PairAdjustmentsCache.Add(hashIdent, pairAdjustments = new()); + try + { + pairAdjustments.AddRange(TrueTypeUtils.ExtractHorizontalPairAdjustments(raw).ToArray()); + } + catch + { + // don't care + } + } + } + + foreach (var pair in pairAdjustments) + { + if (!ImGuiHelpers.IsCodepointInSuppliedGlyphRangesUnsafe(pair.Left, raw.GlyphRanges)) + continue; + if (!ImGuiHelpers.IsCodepointInSuppliedGlyphRangesUnsafe(pair.Right, raw.GlyphRanges)) + continue; + + font.AddKerningPair(pair.Left, pair.Right, pair.Distance * raw.SizePixels); + } + + return font; + } + catch + { + if (freeOnException) + ImGuiNative.igMemFree(dataPointer); + throw; + } + } + + /// + public ImFontPtr AddFontFromFile(string path, in SafeFontConfig fontConfig) + { + return this.AddFontFromStream( + File.OpenRead(path), + fontConfig, + false, + $"{nameof(this.AddFontFromFile)}({path})"); + } + + /// + public unsafe ImFontPtr AddFontFromStream( + Stream stream, + in SafeFontConfig fontConfig, + bool leaveOpen, + string debugTag) + { + using var streamCloser = leaveOpen ? null : stream; + if (!stream.CanSeek) + { + // There is no need to dispose a MemoryStream. + var ms = new MemoryStream(); + stream.CopyTo(ms); + stream = ms; + } + + var length = checked((int)(uint)stream.Length); + var memory = ImGuiHelpers.AllocateMemory(length); + try + { + stream.ReadExactly(new(memory, length)); + return this.AddFontFromImGuiHeapAllocatedMemory( + memory, + length, + fontConfig, + false, + $"{nameof(this.AddFontFromStream)}({debugTag})"); + } + catch + { + ImGuiNative.igMemFree(memory); + throw; + } + } + + /// + public unsafe ImFontPtr AddFontFromMemory( + ReadOnlySpan span, + in SafeFontConfig fontConfig, + string debugTag) + { + var length = span.Length; + var memory = ImGuiHelpers.AllocateMemory(length); + try + { + span.CopyTo(new(memory, length)); + return this.AddFontFromImGuiHeapAllocatedMemory( + memory, + length, + fontConfig, + false, + $"{nameof(this.AddFontFromMemory)}({debugTag})"); + } + catch + { + ImGuiNative.igMemFree(memory); + throw; + } + } + + /// + public ImFontPtr AddDalamudDefaultFont(float sizePx, ushort[]? glyphRanges) + { + ImFontPtr font; + glyphRanges ??= this.factory.DefaultGlyphRanges; + if (this.factory.UseAxis) + { + font = this.AddGameGlyphs(new(GameFontFamily.Axis, sizePx), glyphRanges, default); + } + else + { + font = this.AddDalamudAssetFont( + DalamudAsset.NotoSansJpMedium, + new() { SizePx = sizePx, GlyphRanges = glyphRanges }); + this.AddGameSymbol(new() { SizePx = sizePx, MergeFont = font }); + } + + this.AttachExtraGlyphsForDalamudLanguage(new() { SizePx = sizePx, MergeFont = font }); + if (this.Font.IsNull()) + this.Font = font; + return font; + } + + /// + public ImFontPtr AddDalamudAssetFont(DalamudAsset asset, in SafeFontConfig fontConfig) + { + if (asset.GetPurpose() != DalamudAssetPurpose.Font) + throw new ArgumentOutOfRangeException(nameof(asset), asset, "Must have the purpose of Font."); + + switch (asset) + { + case DalamudAsset.LodestoneGameSymbol when this.factory.HasGameSymbolsFontFile: + return this.factory.AddFont( + this, + asset, + fontConfig with + { + FontNo = 0, + SizePx = (fontConfig.SizePx * 3) / 2, + }); + + case DalamudAsset.LodestoneGameSymbol when !this.factory.HasGameSymbolsFontFile: + { + return this.AddGameGlyphs( + new(GameFontFamily.Axis, fontConfig.SizePx), + fontConfig.GlyphRanges, + fontConfig.MergeFont); + } + + default: + return this.factory.AddFont( + this, + asset, + fontConfig with + { + FontNo = 0, + }); + } + } + + /// + public ImFontPtr AddFontAwesomeIconFont(in SafeFontConfig fontConfig) => this.AddDalamudAssetFont( + DalamudAsset.FontAwesomeFreeSolid, + fontConfig with + { + GlyphRanges = new ushort[] { FontAwesomeIconMin, FontAwesomeIconMax, 0 }, + }); + + /// + public ImFontPtr AddGameSymbol(in SafeFontConfig fontConfig) => + this.AddDalamudAssetFont( + DalamudAsset.LodestoneGameSymbol, + fontConfig with + { + GlyphRanges = new ushort[] + { + GamePrebakedFontHandle.SeIconCharMin, + GamePrebakedFontHandle.SeIconCharMax, + 0, + }, + }); + + /// + public ImFontPtr AddGameGlyphs(GameFontStyle gameFontStyle, ushort[]? glyphRanges, ImFontPtr mergeFont) => + this.gameFontHandleSubstance.AttachGameGlyphs(this, mergeFont, gameFontStyle, glyphRanges); + + /// + public void AttachExtraGlyphsForDalamudLanguage(in SafeFontConfig fontConfig) + { + var dalamudConfiguration = Service.Get(); + if (dalamudConfiguration.EffectiveLanguage == "ko" + || Service.GetNullable()?.EncounteredHangul is true) + { + this.AddDalamudAssetFont( + DalamudAsset.NotoSansKrRegular, + fontConfig with + { + GlyphRanges = ImGuiHelpers.CreateImGuiRangesFrom( + UnicodeRanges.HangulJamo, + UnicodeRanges.HangulCompatibilityJamo, + UnicodeRanges.HangulSyllables, + UnicodeRanges.HangulJamoExtendedA, + UnicodeRanges.HangulJamoExtendedB), + }); + } + + var windowsDir = Environment.GetFolderPath(Environment.SpecialFolder.Windows); + var fontPathChs = Path.Combine(windowsDir, "Fonts", "msyh.ttc"); + if (!File.Exists(fontPathChs)) + fontPathChs = null; + + var fontPathCht = Path.Combine(windowsDir, "Fonts", "msjh.ttc"); + if (!File.Exists(fontPathCht)) + fontPathCht = null; + + if (fontPathCht != null && Service.Get().EffectiveLanguage == "tw") + { + this.AddFontFromFile(fontPathCht, fontConfig with + { + GlyphRanges = ImGuiHelpers.CreateImGuiRangesFrom( + UnicodeRanges.CjkUnifiedIdeographs, + UnicodeRanges.CjkUnifiedIdeographsExtensionA), + }); + } + else if (fontPathChs != null && (Service.Get().EffectiveLanguage == "zh" + || Service.GetNullable()?.EncounteredHan is true)) + { + this.AddFontFromFile(fontPathChs, fontConfig with + { + GlyphRanges = ImGuiHelpers.CreateImGuiRangesFrom( + UnicodeRanges.CjkUnifiedIdeographs, + UnicodeRanges.CjkUnifiedIdeographsExtensionA), + }); + } + } + + public void PreBuildSubstances() + { + foreach (var substance in this.data.Substances) + substance.OnPreBuild(this); + foreach (var substance in this.data.Substances) + substance.OnPreBuildCleanup(this); + } + + public unsafe void PreBuild() + { + var configData = this.data.ConfigData; + foreach (ref var config in configData.DataSpan) + { + if (this.GlobalScaleExclusions.Contains(new(config.DstFont))) + continue; + + config.SizePixels *= this.Scale; + + config.GlyphMaxAdvanceX *= this.Scale; + if (float.IsInfinity(config.GlyphMaxAdvanceX)) + config.GlyphMaxAdvanceX = config.GlyphMaxAdvanceX > 0 ? float.MaxValue : -float.MaxValue; + + config.GlyphMinAdvanceX *= this.Scale; + if (float.IsInfinity(config.GlyphMinAdvanceX)) + config.GlyphMinAdvanceX = config.GlyphMinAdvanceX > 0 ? float.MaxValue : -float.MaxValue; + + config.GlyphOffset *= this.Scale; + } + } + + public void DoBuild() + { + // ImGui will call AddFontDefault() on Build() call. + // AddFontDefault() will reliably crash, when invoked multithreaded. + // We add a dummy font to prevent that. + if (this.data.ConfigData.Length == 0) + { + this.AddDalamudAssetFont( + DalamudAsset.NotoSansJpMedium, + new() { GlyphRanges = new ushort[] { ' ', ' ', '\0' }, SizePx = 1 }); + } + + if (!this.NewImAtlas.Build()) + throw new InvalidOperationException("ImFontAtlas.Build failed"); + + this.BuildStep = FontAtlasBuildStep.PostBuild; + } + + public unsafe void PostBuild() + { + var scale = this.Scale; + foreach (ref var font in this.Fonts.DataSpan) + { + if (!this.GlobalScaleExclusions.Contains(font)) + font.AdjustGlyphMetrics(1 / scale, 1 / scale); + + foreach (var c in FallbackCodepoints) + { + var g = font.FindGlyphNoFallback(c); + if (g.NativePtr == null) + continue; + + font.UpdateFallbackChar(c); + break; + } + + foreach (var c in EllipsisCodepoints) + { + var g = font.FindGlyphNoFallback(c); + if (g.NativePtr == null) + continue; + + font.EllipsisChar = c; + break; + } + } + } + + public void PostBuildSubstances() + { + foreach (var substance in this.data.Substances) + substance.OnPostBuild(this); + } + + public unsafe void UploadTextures() + { + var buf = Array.Empty(); + try + { + var use4 = this.factory.InterfaceManager.SupportsDxgiFormat(Format.B4G4R4A4_UNorm); + var bpp = use4 ? 2 : 4; + var width = this.NewImAtlas.TexWidth; + var height = this.NewImAtlas.TexHeight; + foreach (ref var texture in this.data.ImTextures.DataSpan) + { + if (texture.TexID != 0) + { + // Nothing to do + } + else if (texture.TexPixelsRGBA32 is not null) + { + var wrap = this.factory.InterfaceManager.LoadImageFromDxgiFormat( + new(texture.TexPixelsRGBA32, width * height * 4), + width * 4, + width, + height, + use4 ? Format.B4G4R4A4_UNorm : Format.R8G8B8A8_UNorm); + this.data.AddExistingTexture(wrap); + texture.TexID = wrap.ImGuiHandle; + } + else if (texture.TexPixelsAlpha8 is not null) + { + var numPixels = width * height; + if (buf.Length < numPixels * bpp) + { + ArrayPool.Shared.Return(buf); + buf = ArrayPool.Shared.Rent(numPixels * bpp); + } + + fixed (void* pBuf = buf) + { + var sourcePtr = texture.TexPixelsAlpha8; + if (use4) + { + var target = (ushort*)pBuf; + while (numPixels-- > 0) + { + *target = (ushort)((*sourcePtr << 8) | 0x0FFF); + target++; + sourcePtr++; + } + } + else + { + var target = (uint*)pBuf; + while (numPixels-- > 0) + { + *target = (uint)((*sourcePtr << 24) | 0x00FFFFFF); + target++; + sourcePtr++; + } + } + } + + var wrap = this.factory.InterfaceManager.LoadImageFromDxgiFormat( + buf, + width * bpp, + width, + height, + use4 ? Format.B4G4R4A4_UNorm : Format.B8G8R8A8_UNorm); + this.data.AddExistingTexture(wrap); + texture.TexID = wrap.ImGuiHandle; + continue; + } + else + { + Log.Warning( + "[{name}]: TexID, TexPixelsRGBA32, and TexPixelsAlpha8 are all null", + this.data.Owner?.Name ?? "(error)"); + } + + if (texture.TexPixelsRGBA32 is not null) + ImGuiNative.igMemFree(texture.TexPixelsRGBA32); + if (texture.TexPixelsAlpha8 is not null) + ImGuiNative.igMemFree(texture.TexPixelsAlpha8); + texture.TexPixelsRGBA32 = null; + texture.TexPixelsAlpha8 = null; + } + } + finally + { + ArrayPool.Shared.Return(buf); + } + } + } + + /// + /// Implementations for . + /// + private class BuildToolkitPostPromotion : IFontAtlasBuildToolkitPostPromotion + { + private readonly FontAtlasBuiltData builtData; + + /// + /// Initializes a new instance of the class. + /// + /// The built data. + public BuildToolkitPostPromotion(FontAtlasBuiltData builtData) => this.builtData = builtData; + + /// + public ImFontPtr Font { get; set; } + + /// + public float Scale => this.builtData.Scale; + + /// + public bool IsAsyncBuildOperation => true; + + /// + public FontAtlasBuildStep BuildStep => FontAtlasBuildStep.PostPromotion; + + /// + public ImFontAtlasPtr NewImAtlas => this.builtData.Atlas; + + /// + public unsafe ImVectorWrapper Fonts => new( + &this.NewImAtlas.NativePtr->Fonts, + x => ImGuiNative.ImFont_destroy(x->NativePtr)); + + /// + public T DisposeWithAtlas(T disposable) where T : IDisposable => this.builtData.Garbage.Add(disposable); + + /// + public GCHandle DisposeWithAtlas(GCHandle gcHandle) => this.builtData.Garbage.Add(gcHandle); + + /// + public void DisposeWithAtlas(Action action) => this.builtData.Garbage.Add(action); + + /// + public unsafe void CopyGlyphsAcrossFonts( + ImFontPtr source, + ImFontPtr target, + bool missingOnly, + bool rebuildLookupTable = true, + char rangeLow = ' ', + char rangeHigh = '\uFFFE') + { + var sourceFound = false; + var targetFound = false; + foreach (var f in this.Fonts) + { + sourceFound |= f.NativePtr == source.NativePtr; + targetFound |= f.NativePtr == target.NativePtr; + } + + if (sourceFound && targetFound) + { + ImGuiHelpers.CopyGlyphsAcrossFonts( + source, + target, + missingOnly, + false, + rangeLow, + rangeHigh); + if (rebuildLookupTable) + this.BuildLookupTable(target); + } + } + + /// + public unsafe void BuildLookupTable(ImFontPtr font) + { + // Need to clear previous Fallback pointers before BuildLookupTable, or it may crash + font.NativePtr->FallbackGlyph = null; + font.NativePtr->FallbackHotData = null; + font.BuildLookupTable(); + + // Need to fix our custom ImGui, so that imgui_widgets.cpp:3656 stops thinking + // Codepoint < FallbackHotData.size always means that it's not fallback char. + // Otherwise, having a fallback character in ImGui.InputText gets strange. + var indexedHotData = font.IndexedHotDataWrapped(); + var indexLookup = font.IndexLookupWrapped(); + ref var fallbackHotData = ref *(ImGuiHelpers.ImFontGlyphHotDataReal*)font.NativePtr->FallbackHotData; + for (var codepoint = 0; codepoint < indexedHotData.Length; codepoint++) + { + if (indexLookup[codepoint] == ushort.MaxValue) + { + indexedHotData[codepoint].AdvanceX = fallbackHotData.AdvanceX; + indexedHotData[codepoint].OccupiedWidth = fallbackHotData.OccupiedWidth; + } + } + } + } +} diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs new file mode 100644 index 000000000..5656fc673 --- /dev/null +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs @@ -0,0 +1,726 @@ +// #define VeryVerboseLog + +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Reactive.Disposables; +using System.Threading; +using System.Threading.Tasks; + +using Dalamud.Interface.GameFonts; +using Dalamud.Interface.Internal; +using Dalamud.Interface.Utility; +using Dalamud.Logging.Internal; +using Dalamud.Utility; + +using ImGuiNET; + +using JetBrains.Annotations; + +namespace Dalamud.Interface.ManagedFontAtlas.Internals; + +/// +/// Standalone font atlas. +/// +internal sealed partial class FontAtlasFactory +{ + /// + /// Fallback codepoints for ImFont. + /// + public const string FallbackCodepoints = "\u3013\uFFFD?-"; + + /// + /// Ellipsis codepoints for ImFont. + /// + public const string EllipsisCodepoints = "\u2026\u0085"; + + /// + /// If set, disables concurrent font build operation. + /// + private static readonly object? NoConcurrentBuildOperationLock = null; // new(); + + private static readonly ModuleLog Log = new(nameof(FontAtlasFactory)); + + private static readonly Task EmptyTask = Task.FromResult(default(FontAtlasBuiltData)); + + private struct FontAtlasBuiltData : IDisposable + { + public readonly DalamudFontAtlas? Owner; + public readonly ImFontAtlasPtr Atlas; + public readonly float Scale; + + public bool IsBuildInProgress; + + private readonly List? wraps; + private readonly List? substances; + private readonly DisposeSafety.ScopedFinalizer? garbage; + + public unsafe FontAtlasBuiltData( + DalamudFontAtlas owner, + IEnumerable substances, + float scale) + { + this.Owner = owner; + this.Scale = scale; + this.garbage = new(); + + try + { + var substancesList = this.substances = new(); + foreach (var s in substances) + substancesList.Add(this.garbage.Add(s)); + this.garbage.Add(() => substancesList.Clear()); + + var wrapsCopy = this.wraps = new(); + this.garbage.Add(() => wrapsCopy.Clear()); + + var atlasPtr = ImGuiNative.ImFontAtlas_ImFontAtlas(); + this.Atlas = atlasPtr; + if (this.Atlas.NativePtr is null) + throw new OutOfMemoryException($"Failed to allocate a new {nameof(ImFontAtlas)}."); + + this.garbage.Add(() => ImGuiNative.ImFontAtlas_destroy(atlasPtr)); + this.IsBuildInProgress = true; + } + catch + { + this.garbage.Dispose(); + throw; + } + } + + public readonly DisposeSafety.ScopedFinalizer Garbage => + this.garbage ?? throw new ObjectDisposedException(nameof(FontAtlasBuiltData)); + + public readonly ImVectorWrapper Fonts => this.Atlas.FontsWrapped(); + + public readonly ImVectorWrapper ConfigData => this.Atlas.ConfigDataWrapped(); + + public readonly ImVectorWrapper ImTextures => this.Atlas.TexturesWrapped(); + + public readonly IReadOnlyList Wraps => + (IReadOnlyList?)this.wraps ?? Array.Empty(); + + public readonly IReadOnlyList Substances => + (IReadOnlyList?)this.substances ?? Array.Empty(); + + public readonly void AddExistingTexture(IDalamudTextureWrap wrap) + { + if (this.wraps is null) + throw new ObjectDisposedException(nameof(FontAtlasBuiltData)); + + this.wraps.Add(this.Garbage.Add(wrap)); + } + + public readonly int AddNewTexture(IDalamudTextureWrap wrap, bool disposeOnError) + { + if (this.wraps is null) + throw new ObjectDisposedException(nameof(FontAtlasBuiltData)); + + var handle = wrap.ImGuiHandle; + var index = this.ImTextures.IndexOf(x => x.TexID == handle); + if (index == -1) + { + try + { + this.wraps.EnsureCapacity(this.wraps.Count + 1); + this.ImTextures.EnsureCapacityExponential(this.ImTextures.Length + 1); + + index = this.ImTextures.Length; + this.wraps.Add(this.Garbage.Add(wrap)); + this.ImTextures.Add(new() { TexID = handle }); + } + catch (Exception e) + { + if (disposeOnError) + wrap.Dispose(); + + if (this.wraps.Count != this.ImTextures.Length) + { + Log.Error( + e, + "{name} failed, and {wraps} and {imtextures} have different number of items", + nameof(this.AddNewTexture), + nameof(this.Wraps), + nameof(this.ImTextures)); + + if (this.wraps.Count > 0 && this.wraps[^1] == wrap) + this.wraps.RemoveAt(this.wraps.Count - 1); + if (this.ImTextures.Length > 0 && this.ImTextures[^1].TexID == handle) + this.ImTextures.RemoveAt(this.ImTextures.Length - 1); + + if (this.wraps.Count != this.ImTextures.Length) + Log.Fatal("^ Failed to undo due to an internal inconsistency; embrace for a crash"); + } + + throw; + } + } + + return index; + } + + public unsafe void Dispose() + { + if (this.garbage is null) + return; + + if (this.IsBuildInProgress) + { + Log.Error( + "[{name}] 0x{ptr:X}: Trying to dispose while build is in progress; waiting for build.\n" + + "Stack:\n{trace}", + this.Owner?.Name ?? "", + (nint)this.Atlas.NativePtr, + new StackTrace()); + while (this.IsBuildInProgress) + Thread.Sleep(100); + } + +#if VeryVerboseLog + Log.Verbose("[{name}] 0x{ptr:X}: Disposing", this.Owner?.Name ?? "", (nint)this.Atlas.NativePtr); +#endif + this.garbage.Dispose(); + } + + public BuildToolkit CreateToolkit(FontAtlasFactory factory, bool isAsync) + { + var axisSubstance = this.Substances.OfType().Single(); + return new(factory, this, axisSubstance, isAsync) { BuildStep = FontAtlasBuildStep.PreBuild }; + } + } + + private class DalamudFontAtlas : IFontAtlas, DisposeSafety.IDisposeCallback + { + private readonly DisposeSafety.ScopedFinalizer disposables = new(); + private readonly FontAtlasFactory factory; + private readonly DelegateFontHandle.HandleManager delegateFontHandleManager; + private readonly GamePrebakedFontHandle.HandleManager gameFontHandleManager; + private readonly IFontHandleManager[] fontHandleManagers; + + private readonly object syncRootPostPromotion = new(); + private readonly object syncRoot = new(); + + private Task buildTask = EmptyTask; + private FontAtlasBuiltData builtData; + + private int buildSuppressionCounter; + private bool buildSuppressionSuppressed; + + private int buildIndex; + private bool buildQueued; + private bool disposed = false; + + /// + /// Initializes a new instance of the class. + /// + /// The factory. + /// Name of atlas, for debugging and logging purposes. + /// Specify how to auto rebuild. + /// Whether the fonts in the atlas are under the effect of global scale. + public DalamudFontAtlas( + FontAtlasFactory factory, + string atlasName, + FontAtlasAutoRebuildMode autoRebuildMode, + bool isGlobalScaled) + { + this.IsGlobalScaled = isGlobalScaled; + try + { + this.factory = factory; + this.AutoRebuildMode = autoRebuildMode; + this.Name = atlasName; + + this.factory.InterfaceManager.AfterBuildFonts += this.OnRebuildRecommend; + this.disposables.Add(() => this.factory.InterfaceManager.AfterBuildFonts -= this.OnRebuildRecommend); + + this.fontHandleManagers = new IFontHandleManager[] + { + this.delegateFontHandleManager = this.disposables.Add( + new DelegateFontHandle.HandleManager(atlasName)), + this.gameFontHandleManager = this.disposables.Add( + new GamePrebakedFontHandle.HandleManager(atlasName, factory)), + }; + foreach (var fhm in this.fontHandleManagers) + fhm.RebuildRecommend += this.OnRebuildRecommend; + } + catch + { + this.disposables.Dispose(); + throw; + } + + this.factory.SceneTask.ContinueWith( + r => + { + lock (this.syncRoot) + { + if (this.disposed) + return; + + r.Result.OnNewRenderFrame += this.ImGuiSceneOnNewRenderFrame; + this.disposables.Add(() => r.Result.OnNewRenderFrame -= this.ImGuiSceneOnNewRenderFrame); + } + + if (this.AutoRebuildMode == FontAtlasAutoRebuildMode.OnNewFrame) + this.BuildFontsOnNextFrame(); + }); + } + + /// + /// Finalizes an instance of the class. + /// + ~DalamudFontAtlas() + { + lock (this.syncRoot) + { + this.buildTask.ToDisposableIgnoreExceptions().Dispose(); + this.builtData.Dispose(); + } + } + + /// + public event FontAtlasBuildStepDelegate? BuildStepChange; + + /// + public event Action? RebuildRecommend; + + /// + public event Action? BeforeDispose; + + /// + public event Action? AfterDispose; + + /// + public string Name { get; } + + /// + public FontAtlasAutoRebuildMode AutoRebuildMode { get; } + + /// + public ImFontAtlasPtr ImAtlas + { + get + { + lock (this.syncRoot) + return this.builtData.Atlas; + } + } + + /// + public Task BuildTask => this.buildTask; + + /// + public bool HasBuiltAtlas => !this.builtData.Atlas.IsNull(); + + /// + public bool IsGlobalScaled { get; } + + /// + public void Dispose() + { + if (this.disposed) + return; + + this.BeforeDispose?.InvokeSafely(this); + + try + { + lock (this.syncRoot) + { + this.disposed = true; + this.buildTask.ToDisposableIgnoreExceptions().Dispose(); + this.buildTask = EmptyTask; + this.disposables.Add(this.builtData); + this.builtData = default; + this.disposables.Dispose(); + } + + try + { + this.AfterDispose?.Invoke(this, null); + } + catch + { + // ignore + } + } + catch (Exception e) + { + try + { + this.AfterDispose?.Invoke(this, e); + } + catch + { + // ignore + } + } + + GC.SuppressFinalize(this); + } + + /// + public IDisposable SuppressAutoRebuild() + { + this.buildSuppressionCounter++; + return Disposable.Create( + () => + { + this.buildSuppressionCounter--; + if (this.buildSuppressionSuppressed) + this.OnRebuildRecommend(); + }); + } + + /// + public IFontHandle NewGameFontHandle(GameFontStyle style) => this.gameFontHandleManager.NewFontHandle(style); + + /// + public IFontHandle NewDelegateFontHandle(FontAtlasBuildStepDelegate buildStepDelegate) => + this.delegateFontHandleManager.NewFontHandle(buildStepDelegate); + + /// + public void BuildFontsOnNextFrame() + { + if (this.AutoRebuildMode == FontAtlasAutoRebuildMode.Async) + { + throw new InvalidOperationException( + $"{nameof(this.BuildFontsOnNextFrame)} cannot be used when " + + $"{nameof(this.AutoRebuildMode)} is set to " + + $"{nameof(FontAtlasAutoRebuildMode.Async)}."); + } + + if (!this.buildTask.IsCompleted || this.buildQueued) + return; + +#if VeryVerboseLog + Log.Verbose("[{name}] Queueing from {source}.", this.Name, nameof(this.BuildFontsOnNextFrame)); +#endif + + this.buildQueued = true; + } + + /// + public void BuildFontsImmediately() + { +#if VeryVerboseLog + Log.Verbose("[{name}] Called: {source}.", this.Name, nameof(this.BuildFontsImmediately)); +#endif + + if (this.AutoRebuildMode == FontAtlasAutoRebuildMode.Async) + { + throw new InvalidOperationException( + $"{nameof(this.BuildFontsImmediately)} cannot be used when " + + $"{nameof(this.AutoRebuildMode)} is set to " + + $"{nameof(FontAtlasAutoRebuildMode.Async)}."); + } + + var tcs = new TaskCompletionSource(); + int rebuildIndex; + try + { + rebuildIndex = ++this.buildIndex; + lock (this.syncRoot) + { + if (!this.buildTask.IsCompleted) + throw new InvalidOperationException("Font rebuild is already in progress."); + + this.buildTask = tcs.Task; + } + +#if VeryVerboseLog + Log.Verbose("[{name}] Building from {source}.", this.Name, nameof(this.BuildFontsImmediately)); +#endif + + var scale = this.IsGlobalScaled ? ImGuiHelpers.GlobalScaleSafe : 1f; + var r = this.RebuildFontsPrivate(false, scale); + r.Wait(); + if (r.IsCompletedSuccessfully) + tcs.SetResult(r.Result); + else if (r.Exception is not null) + tcs.SetException(r.Exception); + else + tcs.SetCanceled(); + } + catch (Exception e) + { + tcs.SetException(e); + Log.Error(e, "[{name}] Failed to build fonts.", this.Name); + throw; + } + + this.InvokePostPromotion(rebuildIndex, tcs.Task.Result, nameof(this.BuildFontsImmediately)); + } + + /// + public Task BuildFontsAsync(bool callPostPromotionOnMainThread = true) + { +#if VeryVerboseLog + Log.Verbose("[{name}] Called: {source}.", this.Name, nameof(this.BuildFontsAsync)); +#endif + + if (this.AutoRebuildMode == FontAtlasAutoRebuildMode.OnNewFrame) + { + throw new InvalidOperationException( + $"{nameof(this.BuildFontsAsync)} cannot be used when " + + $"{nameof(this.AutoRebuildMode)} is set to " + + $"{nameof(FontAtlasAutoRebuildMode.OnNewFrame)}."); + } + + lock (this.syncRoot) + { + var scale = this.IsGlobalScaled ? ImGuiHelpers.GlobalScaleSafe : 1f; + var rebuildIndex = ++this.buildIndex; + return this.buildTask = this.buildTask.ContinueWith(BuildInner).Unwrap(); + + async Task BuildInner(Task unused) + { + Log.Verbose("[{name}] Building from {source}.", this.Name, nameof(this.BuildFontsAsync)); + lock (this.syncRoot) + { + if (this.buildIndex != rebuildIndex) + return default; + } + + var res = await this.RebuildFontsPrivate(true, scale); + if (res.Atlas.IsNull()) + return res; + + if (callPostPromotionOnMainThread) + { + await this.factory.Framework.RunOnFrameworkThread( + () => this.InvokePostPromotion(rebuildIndex, res, nameof(this.BuildFontsAsync))); + } + else + { + this.InvokePostPromotion(rebuildIndex, res, nameof(this.BuildFontsAsync)); + } + + return res; + } + } + } + + private void InvokePostPromotion(int rebuildIndex, FontAtlasBuiltData data, [UsedImplicitly] string source) + { + lock (this.syncRoot) + { + if (this.buildIndex != rebuildIndex) + { + data.ExplicitDisposeIgnoreExceptions(); + return; + } + + this.builtData.ExplicitDisposeIgnoreExceptions(); + this.builtData = data; + this.buildTask = EmptyTask; + foreach (var substance in data.Substances) + substance.Manager.Substance = substance; + } + + lock (this.syncRootPostPromotion) + { + if (this.buildIndex != rebuildIndex) + { + data.ExplicitDisposeIgnoreExceptions(); + return; + } + + var toolkit = new BuildToolkitPostPromotion(data); + + try + { + this.BuildStepChange?.Invoke(toolkit); + } + catch (Exception e) + { + Log.Error( + e, + "[{name}] {delegateName} PostPromotion error", + this.Name, + nameof(FontAtlasBuildStepDelegate)); + } + + foreach (var substance in data.Substances) + { + try + { + substance.OnPostPromotion(toolkit); + } + catch (Exception e) + { + Log.Error( + e, + "[{name}] {substance} PostPromotion error", + this.Name, + substance.GetType().FullName ?? substance.GetType().Name); + } + } + + foreach (var font in toolkit.Fonts) + { + try + { + toolkit.BuildLookupTable(font); + } + catch (Exception e) + { + Log.Error(e, "[{name}] BuildLookupTable error", this.Name); + } + } + +#if VeryVerboseLog + Log.Verbose("[{name}] Built from {source}.", this.Name, source); +#endif + } + } + + private void ImGuiSceneOnNewRenderFrame() + { + if (!this.buildQueued) + return; + + try + { + if (this.AutoRebuildMode != FontAtlasAutoRebuildMode.Async) + this.BuildFontsImmediately(); + } + finally + { + this.buildQueued = false; + } + } + + private Task RebuildFontsPrivate(bool isAsync, float scale) + { + if (NoConcurrentBuildOperationLock is null) + return this.RebuildFontsPrivateReal(isAsync, scale); + lock (NoConcurrentBuildOperationLock) + return this.RebuildFontsPrivateReal(isAsync, scale); + } + + private async Task RebuildFontsPrivateReal(bool isAsync, float scale) + { + lock (this.syncRoot) + { + // this lock ensures that this.buildTask is properly set. + } + + var sw = new Stopwatch(); + sw.Start(); + + var res = default(FontAtlasBuiltData); + nint atlasPtr = 0; + try + { + res = new(this, this.fontHandleManagers.Select(x => x.NewSubstance()), scale); + unsafe + { + atlasPtr = (nint)res.Atlas.NativePtr; + } + + Log.Verbose( + "[{name}:{functionname}] 0x{ptr:X}: PreBuild (at {sw}ms)", + this.Name, + nameof(this.RebuildFontsPrivateReal), + atlasPtr, + sw.ElapsedMilliseconds); + + using var toolkit = res.CreateToolkit(this.factory, isAsync); + this.BuildStepChange?.Invoke(toolkit); + toolkit.PreBuildSubstances(); + toolkit.PreBuild(); + +#if VeryVerboseLog + Log.Verbose("[{name}:{functionname}] 0x{ptr:X}: Build (at {sw}ms)", this.Name, nameof(this.RebuildFontsPrivateReal), atlasPtr, sw.ElapsedMilliseconds); +#endif + + toolkit.DoBuild(); + +#if VeryVerboseLog + Log.Verbose("[{name}:{functionname}] 0x{ptr:X}: PostBuild (at {sw}ms)", this.Name, nameof(this.RebuildFontsPrivateReal), atlasPtr, sw.ElapsedMilliseconds); +#endif + + toolkit.PostBuild(); + toolkit.PostBuildSubstances(); + this.BuildStepChange?.Invoke(toolkit); + + if (this.factory.SceneTask is { IsCompleted: false } sceneTask) + { + Log.Verbose( + "[{name}:{functionname}] 0x{ptr:X}: await SceneTask (at {sw}ms)", + this.Name, + nameof(this.RebuildFontsPrivateReal), + atlasPtr, + sw.ElapsedMilliseconds); + await sceneTask.ConfigureAwait(!isAsync); + } + +#if VeryVerboseLog + Log.Verbose("[{name}:{functionname}] 0x{ptr:X}: UploadTextures (at {sw}ms)", this.Name, nameof(this.RebuildFontsPrivateReal), atlasPtr, sw.ElapsedMilliseconds); +#endif + toolkit.UploadTextures(); + + Log.Verbose( + "[{name}:{functionname}] 0x{ptr:X}: Complete (at {sw}ms)", + this.Name, + nameof(this.RebuildFontsPrivateReal), + atlasPtr, + sw.ElapsedMilliseconds); + + res.IsBuildInProgress = false; + return res; + } + catch (Exception e) + { + Log.Error( + e, + "[{name}:{functionname}] 0x{ptr:X}: Failed (at {sw}ms)", + this.Name, + nameof(this.RebuildFontsPrivateReal), + atlasPtr, + sw.ElapsedMilliseconds); + res.IsBuildInProgress = false; + res.Dispose(); + throw; + } + finally + { + this.buildQueued = false; + } + } + + private void OnRebuildRecommend() + { + if (this.disposed) + return; + + if (this.buildSuppressionCounter > 0) + { + this.buildSuppressionSuppressed = true; + return; + } + + this.buildSuppressionSuppressed = false; + this.factory.Framework.RunOnFrameworkThread( + () => + { + this.RebuildRecommend?.InvokeSafely(); + + switch (this.AutoRebuildMode) + { + case FontAtlasAutoRebuildMode.Async: + _ = this.BuildFontsAsync(); + break; + case FontAtlasAutoRebuildMode.OnNewFrame: + this.BuildFontsOnNextFrame(); + break; + case FontAtlasAutoRebuildMode.Disable: + default: + break; + } + }); + } + } +} diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs new file mode 100644 index 000000000..358ccd845 --- /dev/null +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs @@ -0,0 +1,368 @@ +using System.Buffers; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +using Dalamud.Configuration.Internal; +using Dalamud.Data; +using Dalamud.Game; +using Dalamud.Interface.GameFonts; +using Dalamud.Interface.Internal; +using Dalamud.Storage.Assets; +using Dalamud.Utility; + +using ImGuiNET; + +using ImGuiScene; + +using Lumina.Data.Files; + +using SharpDX; +using SharpDX.Direct3D11; +using SharpDX.DXGI; + +namespace Dalamud.Interface.ManagedFontAtlas.Internals; + +/// +/// Factory for the implementation of . +/// +[ServiceManager.BlockingEarlyLoadedService] +internal sealed partial class FontAtlasFactory + : IServiceType, GamePrebakedFontHandle.IGameFontTextureProvider, IDisposable +{ + private readonly DisposeSafety.ScopedFinalizer scopedFinalizer = new(); + private readonly CancellationTokenSource cancellationTokenSource = new(); + private readonly IReadOnlyDictionary> fdtFiles; + private readonly IReadOnlyDictionary[]>> texFiles; + private readonly IReadOnlyDictionary> prebakedTextureWraps; + private readonly Task defaultGlyphRanges; + private readonly DalamudAssetManager dalamudAssetManager; + + [ServiceManager.ServiceConstructor] + private FontAtlasFactory( + DataManager dataManager, + Framework framework, + InterfaceManager interfaceManager, + DalamudAssetManager dalamudAssetManager) + { + this.Framework = framework; + this.InterfaceManager = interfaceManager; + this.dalamudAssetManager = dalamudAssetManager; + this.SceneTask = Service + .GetAsync() + .ContinueWith(r => r.Result.Manager.Scene); + + var gffasInfo = Enum.GetValues() + .Select( + x => + ( + Font: x, + Attr: x.GetAttribute())) + .Where(x => x.Attr is not null) + .ToArray(); + var texPaths = gffasInfo.Select(x => x.Attr.TexPathFormat).Distinct().ToArray(); + + this.fdtFiles = gffasInfo.ToImmutableDictionary( + x => x.Font, + x => Task.Run(() => dataManager.GetFile(x.Attr.Path)!.Data)); + var channelCountsTask = texPaths.ToImmutableDictionary( + x => x, + x => Task.WhenAll( + gffasInfo.Where(y => y.Attr.TexPathFormat == x) + .Select(y => this.fdtFiles[y.Font])) + .ContinueWith( + files => 1 + files.Result.Max( + file => + { + unsafe + { + using var pin = file.AsMemory().Pin(); + var fdt = new FdtFileView(pin.Pointer, file.Length); + return fdt.MaxTextureIndex; + } + }))); + this.prebakedTextureWraps = channelCountsTask.ToImmutableDictionary( + x => x.Key, + x => x.Value.ContinueWith(y => new IDalamudTextureWrap?[y.Result])); + this.texFiles = channelCountsTask.ToImmutableDictionary( + x => x.Key, + x => x.Value.ContinueWith( + y => Enumerable + .Range(1, 1 + ((y.Result - 1) / 4)) + .Select(z => Task.Run(() => dataManager.GetFile(string.Format(x.Key, z))!)) + .ToArray())); + this.defaultGlyphRanges = + this.fdtFiles[GameFontFamilyAndSize.Axis12] + .ContinueWith( + file => + { + unsafe + { + using var pin = file.Result.AsMemory().Pin(); + var fdt = new FdtFileView(pin.Pointer, file.Result.Length); + return fdt.ToGlyphRanges(); + } + }); + } + + /// + /// Gets or sets a value indicating whether to override configuration for UseAxis. + /// + public bool? UseAxisOverride { get; set; } = null; + + /// + /// Gets a value indicating whether to use AXIS fonts. + /// + public bool UseAxis => this.UseAxisOverride ?? Service.Get().UseAxisFontsFromGame; + + /// + /// Gets the service instance of . + /// + public Framework Framework { get; } + + /// + /// Gets the service instance of .
+ /// may not yet be available. + ///
+ public InterfaceManager InterfaceManager { get; } + + /// + /// Gets the async task for inside . + /// + public Task SceneTask { get; } + + /// + /// Gets the default glyph ranges (glyph ranges of ). + /// + public ushort[] DefaultGlyphRanges => ExtractResult(this.defaultGlyphRanges); + + /// + /// Gets a value indicating whether game symbol font file is available. + /// + public bool HasGameSymbolsFontFile => + this.dalamudAssetManager.IsStreamImmediatelyAvailable(DalamudAsset.LodestoneGameSymbol); + + /// + public void Dispose() + { + this.cancellationTokenSource.Cancel(); + this.scopedFinalizer.Dispose(); + this.cancellationTokenSource.Dispose(); + } + + /// + /// Creates a new instance of a class that implements the interface. + /// + /// Name of atlas, for debugging and logging purposes. + /// Specify how to auto rebuild. + /// Whether the fonts in the atlas is global scaled. + /// The new font atlas. + public IFontAtlas CreateFontAtlas( + string atlasName, + FontAtlasAutoRebuildMode autoRebuildMode, + bool isGlobalScaled = true) => + new DalamudFontAtlas(this, atlasName, autoRebuildMode, isGlobalScaled); + + /// + /// Adds the font from Dalamud Assets. + /// + /// The toolkitPostBuild. + /// The font. + /// The font config. + /// The address and size. + public ImFontPtr AddFont( + IFontAtlasBuildToolkitPreBuild toolkitPreBuild, + DalamudAsset asset, + in SafeFontConfig fontConfig) => + toolkitPreBuild.AddFontFromStream( + this.dalamudAssetManager.CreateStream(asset), + fontConfig, + false, + $"Asset({asset})"); + + /// + /// Gets the for the . + /// + /// The font family and size. + /// The . + public FdtReader GetFdtReader(GameFontFamilyAndSize gffas) => new(ExtractResult(this.fdtFiles[gffas])); + + /// + public unsafe MemoryHandle CreateFdtFileView(GameFontFamilyAndSize gffas, out FdtFileView fdtFileView) + { + var arr = ExtractResult(this.fdtFiles[gffas]); + var handle = arr.AsMemory().Pin(); + try + { + fdtFileView = new(handle.Pointer, arr.Length); + return handle; + } + catch + { + handle.Dispose(); + throw; + } + } + + /// + public int GetFontTextureCount(string texPathFormat) => + ExtractResult(this.prebakedTextureWraps[texPathFormat]).Length; + + /// + public TexFile GetTexFile(string texPathFormat, int index) => + ExtractResult(ExtractResult(this.texFiles[texPathFormat])[index]); + + /// + public IDalamudTextureWrap NewFontTextureRef(string texPathFormat, int textureIndex) + { + lock (this.prebakedTextureWraps[texPathFormat]) + { + var wraps = ExtractResult(this.prebakedTextureWraps[texPathFormat]); + var fileIndex = textureIndex / 4; + var channelIndex = FdtReader.FontTableEntry.TextureChannelOrder[textureIndex % 4]; + wraps[textureIndex] ??= this.GetChannelTexture(texPathFormat, fileIndex, channelIndex); + return CloneTextureWrap(wraps[textureIndex]); + } + } + + private static T ExtractResult(Task t) => t.IsCompleted ? t.Result : t.GetAwaiter().GetResult(); + + private static unsafe void ExtractChannelFromB8G8R8A8( + Span target, + ReadOnlySpan source, + int channelIndex, + bool targetIsB4G4R4A4) + { + var numPixels = Math.Min(source.Length / 4, target.Length / (targetIsB4G4R4A4 ? 2 : 4)); + + fixed (byte* sourcePtrImmutable = source) + { + var rptr = sourcePtrImmutable + channelIndex; + fixed (void* targetPtr = target) + { + if (targetIsB4G4R4A4) + { + var wptr = (ushort*)targetPtr; + while (numPixels-- > 0) + { + *wptr = (ushort)((*rptr << 8) | 0x0FFF); + wptr++; + rptr += 4; + } + } + else + { + var wptr = (uint*)targetPtr; + while (numPixels-- > 0) + { + *wptr = (uint)((*rptr << 24) | 0x00FFFFFF); + wptr++; + rptr += 4; + } + } + } + } + } + + /// + /// Clones a texture wrap, by getting a new reference to the underlying and the + /// texture behind. + /// + /// The to clone from. + /// The cloned . + private static IDalamudTextureWrap CloneTextureWrap(IDalamudTextureWrap wrap) + { + var srv = CppObject.FromPointer(wrap.ImGuiHandle); + using var res = srv.Resource; + using var tex2D = res.QueryInterface(); + var description = tex2D.Description; + return new DalamudTextureWrap( + new D3DTextureWrap( + srv.QueryInterface(), + description.Width, + description.Height)); + } + + private static unsafe void ExtractChannelFromB4G4R4A4( + Span target, + ReadOnlySpan source, + int channelIndex, + bool targetIsB4G4R4A4) + { + var numPixels = Math.Min(source.Length / 2, target.Length / (targetIsB4G4R4A4 ? 2 : 4)); + fixed (byte* sourcePtrImmutable = source) + { + var rptr = sourcePtrImmutable + (channelIndex / 2); + var rshift = (channelIndex & 1) == 0 ? 0 : 4; + fixed (void* targetPtr = target) + { + if (targetIsB4G4R4A4) + { + var wptr = (ushort*)targetPtr; + while (numPixels-- > 0) + { + *wptr = (ushort)(((*rptr >> rshift) << 12) | 0x0FFF); + wptr++; + rptr += 2; + } + } + else + { + var wptr = (uint*)targetPtr; + while (numPixels-- > 0) + { + var v = (*rptr >> rshift) & 0xF; + v |= v << 4; + *wptr = (uint)((v << 24) | 0x00FFFFFF); + wptr++; + rptr += 4; + } + } + } + } + } + + private IDalamudTextureWrap GetChannelTexture(string texPathFormat, int fileIndex, int channelIndex) + { + var texFile = ExtractResult(ExtractResult(this.texFiles[texPathFormat])[fileIndex]); + var numPixels = texFile.Header.Width * texFile.Header.Height; + + _ = Service.Get(); + var targetIsB4G4R4A4 = this.InterfaceManager.SupportsDxgiFormat(Format.B4G4R4A4_UNorm); + var bpp = targetIsB4G4R4A4 ? 2 : 4; + var buffer = ArrayPool.Shared.Rent(numPixels * bpp); + try + { + var sliceSpan = texFile.SliceSpan(0, 0, out _, out _, out _); + switch (texFile.Header.Format) + { + case TexFile.TextureFormat.B4G4R4A4: + // Game ships with this format. + ExtractChannelFromB4G4R4A4(buffer, sliceSpan, channelIndex, targetIsB4G4R4A4); + break; + case TexFile.TextureFormat.B8G8R8A8: + // In case of modded font textures. + ExtractChannelFromB8G8R8A8(buffer, sliceSpan, channelIndex, targetIsB4G4R4A4); + break; + default: + // Unlikely. + ExtractChannelFromB8G8R8A8(buffer, texFile.ImageData, channelIndex, targetIsB4G4R4A4); + break; + } + + return this.scopedFinalizer.Add( + this.InterfaceManager.LoadImageFromDxgiFormat( + buffer, + texFile.Header.Width * bpp, + texFile.Header.Width, + texFile.Header.Height, + targetIsB4G4R4A4 ? Format.B4G4R4A4_UNorm : Format.B8G8R8A8_UNorm)); + } + finally + { + ArrayPool.Shared.Return(buffer); + } + } +} diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/GamePrebakedFontHandle.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/GamePrebakedFontHandle.cs new file mode 100644 index 000000000..99c817a91 --- /dev/null +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/GamePrebakedFontHandle.cs @@ -0,0 +1,857 @@ +using System.Buffers; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Reactive.Disposables; + +using Dalamud.Game.Text; +using Dalamud.Interface.GameFonts; +using Dalamud.Interface.Internal; +using Dalamud.Interface.Utility; +using Dalamud.Interface.Utility.Raii; +using Dalamud.Utility; + +using ImGuiNET; + +using Lumina.Data.Files; + +using Vector4 = System.Numerics.Vector4; + +namespace Dalamud.Interface.ManagedFontAtlas.Internals; + +/// +/// A font handle that uses the game's built-in fonts, optionally with some styling. +/// +internal class GamePrebakedFontHandle : IFontHandle.IInternal +{ + /// + /// The smallest value of . + /// + public static readonly char SeIconCharMin = (char)Enum.GetValues().Min(); + + /// + /// The largest value of . + /// + public static readonly char SeIconCharMax = (char)Enum.GetValues().Max(); + + private IFontHandleManager? manager; + + /// + /// Initializes a new instance of the class. + /// + /// An instance of . + /// Font to use. + public GamePrebakedFontHandle(IFontHandleManager manager, GameFontStyle style) + { + if (!Enum.IsDefined(style.FamilyAndSize) || style.FamilyAndSize == GameFontFamilyAndSize.Undefined) + throw new ArgumentOutOfRangeException(nameof(style), style, null); + + if (style.SizePt <= 0) + throw new ArgumentException($"{nameof(style.SizePt)} must be a positive number.", nameof(style)); + + this.manager = manager; + this.FontStyle = style; + } + + /// + /// Provider for for `common/font/fontNN.tex`. + /// + public interface IGameFontTextureProvider + { + /// + /// Creates the for the .
+ /// Dispose after use. + ///
+ /// The font family and size. + /// The view. + /// Dispose this after use.. + public MemoryHandle CreateFdtFileView(GameFontFamilyAndSize gffas, out FdtFileView fdtFileView); + + /// + /// Gets the number of font textures. + /// + /// Format of .tex path. + /// The number of textures. + public int GetFontTextureCount(string texPathFormat); + + /// + /// Gets the for the given index of a font. + /// + /// Format of .tex path. + /// The index of .tex file. + /// The . + public TexFile GetTexFile(string texPathFormat, int index); + + /// + /// Gets a new reference of the font texture. + /// + /// Format of .tex path. + /// Texture index. + /// The texture. + public IDalamudTextureWrap NewFontTextureRef(string texPathFormat, int textureIndex); + } + + /// + /// Gets the font style. + /// + public GameFontStyle FontStyle { get; } + + /// + public Exception? LoadException => this.ManagerNotDisposed.Substance?.GetBuildException(this); + + /// + public bool Available => this.ImFont.IsNotNullAndLoaded(); + + /// + public ImFontPtr ImFont => this.ManagerNotDisposed.Substance?.GetFontPtr(this) ?? default; + + private IFontHandleManager ManagerNotDisposed => + this.manager ?? throw new ObjectDisposedException(nameof(GamePrebakedFontHandle)); + + /// + public void Dispose() + { + this.manager?.FreeFontHandle(this); + this.manager = null; + } + + /// + public IDisposable Push() => ImRaii.PushFont(this.ImFont, this.Available); + + /// + /// Manager for s. + /// + internal sealed class HandleManager : IFontHandleManager + { + private readonly Dictionary gameFontsRc = new(); + private readonly object syncRoot = new(); + + /// + /// Initializes a new instance of the class. + /// + /// The name of the owner atlas. + /// An instance of . + public HandleManager(string atlasName, IGameFontTextureProvider gameFontTextureProvider) + { + this.GameFontTextureProvider = gameFontTextureProvider; + this.Name = $"{atlasName}:{nameof(GamePrebakedFontHandle)}:Manager"; + } + + /// + public event Action? RebuildRecommend; + + /// + public string Name { get; } + + /// + public IFontHandleSubstance? Substance { get; set; } + + /// + /// Gets an instance of . + /// + public IGameFontTextureProvider GameFontTextureProvider { get; } + + /// + public void Dispose() + { + this.Substance?.Dispose(); + this.Substance = null; + } + + /// + public IFontHandle NewFontHandle(GameFontStyle style) + { + var handle = new GamePrebakedFontHandle(this, style); + bool suggestRebuild; + lock (this.syncRoot) + { + this.gameFontsRc[style] = this.gameFontsRc.GetValueOrDefault(style, 0) + 1; + suggestRebuild = this.Substance?.GetFontPtr(handle).IsNotNullAndLoaded() is not true; + } + + if (suggestRebuild) + this.RebuildRecommend?.Invoke(); + + return handle; + } + + /// + public void FreeFontHandle(IFontHandle handle) + { + if (handle is not GamePrebakedFontHandle ggfh) + return; + + lock (this.syncRoot) + { + if (!this.gameFontsRc.ContainsKey(ggfh.FontStyle)) + return; + + if ((this.gameFontsRc[ggfh.FontStyle] -= 1) == 0) + this.gameFontsRc.Remove(ggfh.FontStyle); + } + } + + /// + public IFontHandleSubstance NewSubstance() + { + lock (this.syncRoot) + return new HandleSubstance(this, this.gameFontsRc.Keys); + } + } + + /// + /// Substance from . + /// + internal sealed class HandleSubstance : IFontHandleSubstance + { + private readonly HandleManager handleManager; + private readonly HashSet gameFontStyles; + + // Owned by this class, but ImFontPtr values still do not belong to this. + private readonly Dictionary fonts = new(); + private readonly Dictionary buildExceptions = new(); + private readonly List<(ImFontPtr Font, GameFontStyle Style, ushort[]? Ranges)> attachments = new(); + + private readonly HashSet templatedFonts = new(); + + /// + /// Initializes a new instance of the class. + /// + /// The manager. + /// The game font styles. + public HandleSubstance(HandleManager manager, IEnumerable gameFontStyles) + { + this.handleManager = manager; + Service.Get(); + this.gameFontStyles = new(gameFontStyles); + } + + /// + public IFontHandleManager Manager => this.handleManager; + + /// + public void Dispose() + { + } + + /// + /// Attaches game symbols to the given font. If font is null, it will be created. + /// + /// The toolkitPostBuild. + /// The font to attach to. + /// The game font style. + /// The intended glyph ranges. + /// if it is not empty; otherwise a new font. + public ImFontPtr AttachGameGlyphs( + IFontAtlasBuildToolkitPreBuild toolkitPreBuild, + ImFontPtr font, + GameFontStyle style, + ushort[]? glyphRanges = null) + { + if (font.IsNull()) + font = this.CreateTemplateFont(toolkitPreBuild, style.SizePx); + this.attachments.Add((font, style, glyphRanges)); + return font; + } + + /// + /// Creates or gets a relevant for the given . + /// + /// The game font style. + /// The toolkitPostBuild. + /// The font. + public ImFontPtr GetOrCreateFont(GameFontStyle style, IFontAtlasBuildToolkitPreBuild toolkitPreBuild) + { + try + { + if (!this.fonts.TryGetValue(style, out var plan)) + { + plan = new( + style, + toolkitPreBuild.Scale, + this.handleManager.GameFontTextureProvider, + this.CreateTemplateFont(toolkitPreBuild, style.SizePx)); + this.fonts[style] = plan; + } + + plan.AttachFont(plan.FullRangeFont); + return plan.FullRangeFont; + } + catch (Exception e) + { + this.buildExceptions[style] = e; + throw; + } + } + + /// + public ImFontPtr GetFontPtr(IFontHandle handle) => + handle is GamePrebakedFontHandle ggfh + ? this.fonts.GetValueOrDefault(ggfh.FontStyle)?.FullRangeFont ?? default + : default; + + /// + public Exception? GetBuildException(IFontHandle handle) => + handle is GamePrebakedFontHandle ggfh ? this.buildExceptions.GetValueOrDefault(ggfh.FontStyle) : default; + + /// + public void OnPreBuild(IFontAtlasBuildToolkitPreBuild toolkitPreBuild) + { + foreach (var style in this.gameFontStyles) + { + if (this.fonts.ContainsKey(style)) + continue; + + try + { + _ = this.GetOrCreateFont(style, toolkitPreBuild); + } + catch + { + // ignore; it should have been recorded from the call + } + } + } + + /// + public void OnPreBuildCleanup(IFontAtlasBuildToolkitPreBuild toolkitPreBuild) + { + foreach (var (font, style, ranges) in this.attachments) + { + var effectiveStyle = + toolkitPreBuild.IsGlobalScaleIgnored(font) + ? style.Scale(1 / toolkitPreBuild.Scale) + : style; + if (!this.fonts.TryGetValue(style, out var plan)) + { + plan = new( + effectiveStyle, + toolkitPreBuild.Scale, + this.handleManager.GameFontTextureProvider, + this.CreateTemplateFont(toolkitPreBuild, style.SizePx)); + this.fonts[style] = plan; + } + + plan.AttachFont(font, ranges); + } + + foreach (var plan in this.fonts.Values) + { + plan.EnsureGlyphs(toolkitPreBuild.NewImAtlas); + } + } + + /// + public unsafe void OnPostBuild(IFontAtlasBuildToolkitPostBuild toolkitPostBuild) + { + var allTextureIndices = new Dictionary(); + var allTexFiles = new Dictionary(); + using var rentReturn = Disposable.Create( + () => + { + foreach (var x in allTextureIndices.Values) + ArrayPool.Shared.Return(x); + foreach (var x in allTexFiles.Values) + ArrayPool.Shared.Return(x); + }); + + var pixels8Array = new byte*[toolkitPostBuild.NewImAtlas.Textures.Size]; + var widths = new int[toolkitPostBuild.NewImAtlas.Textures.Size]; + for (var i = 0; i < pixels8Array.Length; i++) + toolkitPostBuild.NewImAtlas.GetTexDataAsAlpha8(i, out pixels8Array[i], out widths[i], out _); + + foreach (var (style, plan) in this.fonts) + { + try + { + foreach (var font in plan.Ranges.Keys) + this.PatchFontMetricsIfNecessary(style, font, toolkitPostBuild.Scale); + + plan.SetFullRangeFontGlyphs(toolkitPostBuild, allTexFiles, allTextureIndices, pixels8Array, widths); + plan.CopyGlyphsToRanges(toolkitPostBuild); + plan.PostProcessFullRangeFont(toolkitPostBuild.Scale); + } + catch (Exception e) + { + this.buildExceptions[style] = e; + this.fonts[style] = default; + } + } + } + + /// + public void OnPostPromotion(IFontAtlasBuildToolkitPostPromotion toolkitPostPromotion) + { + // Irrelevant + } + + /// + /// Creates a new template font. + /// + /// The toolkitPostBuild. + /// The size of the font. + /// The font. + private ImFontPtr CreateTemplateFont(IFontAtlasBuildToolkitPreBuild toolkitPreBuild, float sizePx) + { + var font = toolkitPreBuild.AddDalamudAssetFont( + DalamudAsset.NotoSansJpMedium, + new() + { + GlyphRanges = new ushort[] { ' ', ' ', '\0' }, + SizePx = sizePx, + }); + this.templatedFonts.Add(font); + return font; + } + + private unsafe void PatchFontMetricsIfNecessary(GameFontStyle style, ImFontPtr font, float atlasScale) + { + if (!this.templatedFonts.Contains(font)) + return; + + var fas = style.Scale(atlasScale).FamilyAndSize; + using var handle = this.handleManager.GameFontTextureProvider.CreateFdtFileView(fas, out var fdt); + ref var fdtFontHeader = ref fdt.FontHeader; + var fontPtr = font.NativePtr; + + var scale = style.SizePt / fdtFontHeader.Size; + fontPtr->Ascent = fdtFontHeader.Ascent * scale; + fontPtr->Descent = fdtFontHeader.Descent * scale; + fontPtr->EllipsisChar = '…'; + } + } + + [SuppressMessage( + "StyleCop.CSharp.MaintainabilityRules", + "SA1401:Fields should be private", + Justification = "Internal")] + private sealed class FontDrawPlan : IDisposable + { + public readonly GameFontStyle Style; + public readonly GameFontStyle BaseStyle; + public readonly GameFontFamilyAndSizeAttribute BaseAttr; + public readonly int TexCount; + public readonly Dictionary Ranges = new(); + public readonly List<(int RectId, int FdtGlyphIndex)> Rects = new(); + public readonly ushort[] RectLookup = new ushort[0x10000]; + public readonly FdtFileView Fdt; + public readonly ImFontPtr FullRangeFont; + + private readonly IDisposable fdtHandle; + private readonly IGameFontTextureProvider gftp; + + public FontDrawPlan( + GameFontStyle style, + float scale, + IGameFontTextureProvider gameFontTextureProvider, + ImFontPtr fullRangeFont) + { + this.Style = style; + this.BaseStyle = style.Scale(scale); + this.BaseAttr = this.BaseStyle.FamilyAndSize.GetAttribute()!; + this.gftp = gameFontTextureProvider; + this.TexCount = this.gftp.GetFontTextureCount(this.BaseAttr.TexPathFormat); + this.fdtHandle = this.gftp.CreateFdtFileView(this.BaseStyle.FamilyAndSize, out this.Fdt); + this.RectLookup.AsSpan().Fill(ushort.MaxValue); + this.FullRangeFont = fullRangeFont; + this.Ranges[fullRangeFont] = new(0x10000); + } + + public void Dispose() + { + this.fdtHandle.Dispose(); + } + + public void AttachFont(ImFontPtr font, ushort[]? glyphRanges = null) + { + if (!this.Ranges.TryGetValue(font, out var rangeBitArray)) + rangeBitArray = this.Ranges[font] = new(0x10000); + + if (glyphRanges is null) + { + foreach (ref var g in this.Fdt.Glyphs) + { + var c = g.CharInt; + if (c is >= 0x20 and <= 0xFFFE) + rangeBitArray[c] = true; + } + + return; + } + + for (var i = 0; i < glyphRanges.Length - 1; i += 2) + { + if (glyphRanges[i] == 0) + break; + var from = (int)glyphRanges[i]; + var to = (int)glyphRanges[i + 1]; + for (var j = from; j <= to; j++) + rangeBitArray[j] = true; + } + } + + public unsafe void EnsureGlyphs(ImFontAtlasPtr atlas) + { + var glyphs = this.Fdt.Glyphs; + var ranges = this.Ranges[this.FullRangeFont]; + foreach (var (font, extraRange) in this.Ranges) + { + if (font.NativePtr != this.FullRangeFont.NativePtr) + ranges.Or(extraRange); + } + + if (this.Style is not { Weight: 0, SkewStrength: 0 }) + { + for (var fdtGlyphIndex = 0; fdtGlyphIndex < glyphs.Length; fdtGlyphIndex++) + { + ref var glyph = ref glyphs[fdtGlyphIndex]; + var cint = glyph.CharInt; + if (cint > char.MaxValue) + continue; + if (!ranges[cint] || this.RectLookup[cint] != ushort.MaxValue) + continue; + + var widthAdjustment = this.BaseStyle.CalculateBaseWidthAdjustment(this.Fdt.FontHeader, glyph); + this.RectLookup[cint] = (ushort)this.Rects.Count; + this.Rects.Add( + ( + atlas.AddCustomRectFontGlyph( + this.FullRangeFont, + (char)cint, + glyph.BoundingWidth + widthAdjustment, + glyph.BoundingHeight, + glyph.AdvanceWidth, + new(this.BaseAttr.HorizontalOffset, glyph.CurrentOffsetY)), + fdtGlyphIndex)); + } + } + else + { + for (var fdtGlyphIndex = 0; fdtGlyphIndex < glyphs.Length; fdtGlyphIndex++) + { + ref var glyph = ref glyphs[fdtGlyphIndex]; + var cint = glyph.CharInt; + if (cint > char.MaxValue) + continue; + if (!ranges[cint] || this.RectLookup[cint] != ushort.MaxValue) + continue; + + this.RectLookup[cint] = (ushort)this.Rects.Count; + this.Rects.Add((-1, fdtGlyphIndex)); + } + } + } + + public unsafe void PostProcessFullRangeFont(float atlasScale) + { + var round = 1 / atlasScale; + var pfrf = this.FullRangeFont.NativePtr; + ref var frf = ref *pfrf; + + frf.FontSize = MathF.Round(frf.FontSize / round) * round; + frf.Ascent = MathF.Round(frf.Ascent / round) * round; + frf.Descent = MathF.Round(frf.Descent / round) * round; + + var scale = this.Style.SizePt / this.Fdt.FontHeader.Size; + foreach (ref var g in this.FullRangeFont.GlyphsWrapped().DataSpan) + { + var w = (g.X1 - g.X0) * scale; + var h = (g.Y1 - g.Y0) * scale; + g.X0 = MathF.Round((g.X0 * scale) / round) * round; + g.Y0 = MathF.Round((g.Y0 * scale) / round) * round; + g.X1 = g.X0 + w; + g.Y1 = g.Y0 + h; + g.AdvanceX = MathF.Round((g.AdvanceX * scale) / round) * round; + } + + var fullRange = this.Ranges[this.FullRangeFont]; + foreach (ref var k in this.Fdt.PairAdjustments) + { + var (leftInt, rightInt) = (k.LeftInt, k.RightInt); + if (leftInt > char.MaxValue || rightInt > char.MaxValue) + continue; + if (!fullRange[leftInt] || !fullRange[rightInt]) + continue; + ImGuiNative.ImFont_AddKerningPair( + pfrf, + (ushort)leftInt, + (ushort)rightInt, + MathF.Round((k.RightOffset * scale) / round) * round); + } + + pfrf->FallbackGlyph = null; + ImGuiNative.ImFont_BuildLookupTable(pfrf); + + foreach (var fallbackCharCandidate in FontAtlasFactory.FallbackCodepoints) + { + var glyph = ImGuiNative.ImFont_FindGlyphNoFallback(pfrf, fallbackCharCandidate); + if ((nint)glyph == IntPtr.Zero) + continue; + frf.FallbackChar = fallbackCharCandidate; + frf.FallbackGlyph = glyph; + frf.FallbackHotData = + (ImFontGlyphHotData*)frf.IndexedHotData.Address( + fallbackCharCandidate); + break; + } + } + + public unsafe void CopyGlyphsToRanges(IFontAtlasBuildToolkitPostBuild toolkitPostBuild) + { + var scale = this.Style.SizePt / this.Fdt.FontHeader.Size; + var atlasScale = toolkitPostBuild.Scale; + var round = 1 / atlasScale; + + foreach (var (font, rangeBits) in this.Ranges) + { + if (font.NativePtr == this.FullRangeFont.NativePtr) + continue; + + var noGlobalScale = toolkitPostBuild.IsGlobalScaleIgnored(font); + + var lookup = font.IndexLookupWrapped(); + var glyphs = font.GlyphsWrapped(); + foreach (ref var sourceGlyph in this.FullRangeFont.GlyphsWrapped().DataSpan) + { + if (!rangeBits[sourceGlyph.Codepoint]) + continue; + + var glyphIndex = ushort.MaxValue; + if (sourceGlyph.Codepoint < lookup.Length) + glyphIndex = lookup[sourceGlyph.Codepoint]; + + if (glyphIndex == ushort.MaxValue) + { + glyphIndex = (ushort)glyphs.Length; + glyphs.Add(default); + } + + ref var g = ref glyphs[glyphIndex]; + g = sourceGlyph; + if (noGlobalScale) + { + g.XY *= scale; + g.AdvanceX *= scale; + } + else + { + var w = (g.X1 - g.X0) * scale; + var h = (g.Y1 - g.Y0) * scale; + g.X0 = MathF.Round((g.X0 * scale) / round) * round; + g.Y0 = MathF.Round((g.Y0 * scale) / round) * round; + g.X1 = g.X0 + w; + g.Y1 = g.Y0 + h; + g.AdvanceX = MathF.Round((g.AdvanceX * scale) / round) * round; + } + } + + foreach (ref var k in this.Fdt.PairAdjustments) + { + var (leftInt, rightInt) = (k.LeftInt, k.RightInt); + if (leftInt > char.MaxValue || rightInt > char.MaxValue) + continue; + if (!rangeBits[leftInt] || !rangeBits[rightInt]) + continue; + if (noGlobalScale) + { + font.AddKerningPair((ushort)leftInt, (ushort)rightInt, k.RightOffset * scale); + } + else + { + font.AddKerningPair( + (ushort)leftInt, + (ushort)rightInt, + MathF.Round((k.RightOffset * scale) / round) * round); + } + } + + font.NativePtr->FallbackGlyph = null; + font.BuildLookupTable(); + + foreach (var fallbackCharCandidate in FontAtlasFactory.FallbackCodepoints) + { + var glyph = font.FindGlyphNoFallback(fallbackCharCandidate).NativePtr; + if ((nint)glyph == IntPtr.Zero) + continue; + + ref var frf = ref *font.NativePtr; + frf.FallbackChar = fallbackCharCandidate; + frf.FallbackGlyph = glyph; + frf.FallbackHotData = + (ImFontGlyphHotData*)frf.IndexedHotData.Address( + fallbackCharCandidate); + break; + } + } + } + + public unsafe void SetFullRangeFontGlyphs( + IFontAtlasBuildToolkitPostBuild toolkitPostBuild, + Dictionary allTexFiles, + Dictionary allTextureIndices, + byte*[] pixels8Array, + int[] widths) + { + var glyphs = this.FullRangeFont.GlyphsWrapped(); + var lookups = this.FullRangeFont.IndexLookupWrapped(); + + ref var fdtFontHeader = ref this.Fdt.FontHeader; + var fdtGlyphs = this.Fdt.Glyphs; + var fdtTexSize = new Vector4( + this.Fdt.FontHeader.TextureWidth, + this.Fdt.FontHeader.TextureHeight, + this.Fdt.FontHeader.TextureWidth, + this.Fdt.FontHeader.TextureHeight); + + if (!allTexFiles.TryGetValue(this.BaseAttr.TexPathFormat, out var texFiles)) + { + allTexFiles.Add( + this.BaseAttr.TexPathFormat, + texFiles = ArrayPool.Shared.Rent(this.TexCount)); + } + + if (!allTextureIndices.TryGetValue(this.BaseAttr.TexPathFormat, out var textureIndices)) + { + allTextureIndices.Add( + this.BaseAttr.TexPathFormat, + textureIndices = ArrayPool.Shared.Rent(this.TexCount)); + textureIndices.AsSpan(0, this.TexCount).Fill(-1); + } + + var pixelWidth = Math.Max(1, (int)MathF.Ceiling(this.BaseStyle.Weight + 1)); + var pixelStrength = stackalloc byte[pixelWidth]; + for (var i = 0; i < pixelWidth; i++) + pixelStrength[i] = (byte)(255 * Math.Min(1f, (this.BaseStyle.Weight + 1) - i)); + + var minGlyphY = 0; + var maxGlyphY = 0; + foreach (ref var g in fdtGlyphs) + { + minGlyphY = Math.Min(g.CurrentOffsetY, minGlyphY); + maxGlyphY = Math.Max(g.BoundingHeight + g.CurrentOffsetY, maxGlyphY); + } + + var horzShift = stackalloc int[maxGlyphY - minGlyphY]; + var horzBlend = stackalloc byte[maxGlyphY - minGlyphY]; + horzShift -= minGlyphY; + horzBlend -= minGlyphY; + if (this.BaseStyle.BaseSkewStrength != 0) + { + for (var i = minGlyphY; i < maxGlyphY; i++) + { + float blend = this.BaseStyle.BaseSkewStrength switch + { + > 0 => fdtFontHeader.LineHeight - i, + < 0 => -i, + _ => throw new InvalidOperationException(), + }; + blend *= this.BaseStyle.BaseSkewStrength / fdtFontHeader.LineHeight; + horzShift[i] = (int)MathF.Floor(blend); + horzBlend[i] = (byte)(255 * (blend - horzShift[i])); + } + } + + foreach (var (rectId, fdtGlyphIndex) in this.Rects) + { + ref var fdtGlyph = ref fdtGlyphs[fdtGlyphIndex]; + if (rectId == -1) + { + ref var textureIndex = ref textureIndices[fdtGlyph.TextureIndex]; + if (textureIndex == -1) + { + textureIndex = toolkitPostBuild.StoreTexture( + this.gftp.NewFontTextureRef(this.BaseAttr.TexPathFormat, fdtGlyph.TextureIndex), + true); + } + + var glyph = new ImGuiHelpers.ImFontGlyphReal + { + AdvanceX = fdtGlyph.AdvanceWidth, + Codepoint = fdtGlyph.Char, + Colored = false, + TextureIndex = textureIndex, + Visible = true, + X0 = this.BaseAttr.HorizontalOffset, + Y0 = fdtGlyph.CurrentOffsetY, + U0 = fdtGlyph.TextureOffsetX, + V0 = fdtGlyph.TextureOffsetY, + U1 = fdtGlyph.BoundingWidth, + V1 = fdtGlyph.BoundingHeight, + }; + + glyph.XY1 = glyph.XY0 + glyph.UV1; + glyph.UV1 += glyph.UV0; + glyph.UV /= fdtTexSize; + + glyphs.Add(glyph); + } + else + { + ref var rc = ref *(ImGuiHelpers.ImFontAtlasCustomRectReal*)toolkitPostBuild.NewImAtlas + .GetCustomRectByIndex(rectId) + .NativePtr; + var widthAdjustment = this.BaseStyle.CalculateBaseWidthAdjustment(fdtFontHeader, fdtGlyph); + + // Glyph is scaled at this point; undo that. + ref var glyph = ref glyphs[lookups[rc.GlyphId]]; + glyph.X0 = this.BaseAttr.HorizontalOffset; + glyph.Y0 = fdtGlyph.CurrentOffsetY; + glyph.X1 = glyph.X0 + fdtGlyph.BoundingWidth + widthAdjustment; + glyph.Y1 = glyph.Y0 + fdtGlyph.BoundingHeight; + glyph.AdvanceX = fdtGlyph.AdvanceWidth; + + var pixels8 = pixels8Array[rc.TextureIndex]; + var width = widths[rc.TextureIndex]; + texFiles[fdtGlyph.TextureFileIndex] ??= + this.gftp.GetTexFile(this.BaseAttr.TexPathFormat, fdtGlyph.TextureFileIndex); + var sourceBuffer = texFiles[fdtGlyph.TextureFileIndex].ImageData; + var sourceBufferDelta = fdtGlyph.TextureChannelByteIndex; + + for (var y = 0; y < fdtGlyph.BoundingHeight; y++) + { + var sourcePixelIndex = + ((fdtGlyph.TextureOffsetY + y) * fdtFontHeader.TextureWidth) + fdtGlyph.TextureOffsetX; + sourcePixelIndex *= 4; + sourcePixelIndex += sourceBufferDelta; + var blend1 = horzBlend[fdtGlyph.CurrentOffsetY + y]; + + var targetOffset = ((rc.Y + y) * width) + rc.X; + for (var x = 0; x < rc.Width; x++) + pixels8[targetOffset + x] = 0; + + targetOffset += horzShift[fdtGlyph.CurrentOffsetY + y]; + if (blend1 == 0) + { + for (var x = 0; x < fdtGlyph.BoundingWidth; x++, sourcePixelIndex += 4, targetOffset++) + { + var n = sourceBuffer[sourcePixelIndex + 4]; + for (var boldOffset = 0; boldOffset < pixelWidth; boldOffset++) + { + ref var p = ref pixels8[targetOffset + boldOffset]; + p = Math.Max(p, (byte)((pixelStrength[boldOffset] * n) / 255)); + } + } + } + else + { + var blend2 = 255 - blend1; + for (var x = 0; x < fdtGlyph.BoundingWidth; x++, sourcePixelIndex += 4, targetOffset++) + { + var a1 = sourceBuffer[sourcePixelIndex]; + var a2 = x == fdtGlyph.BoundingWidth - 1 ? 0 : sourceBuffer[sourcePixelIndex + 4]; + var n = (a1 * blend1) + (a2 * blend2); + + for (var boldOffset = 0; boldOffset < pixelWidth; boldOffset++) + { + ref var p = ref pixels8[targetOffset + boldOffset]; + p = Math.Max(p, (byte)((pixelStrength[boldOffset] * n) / 255 / 255)); + } + } + } + } + } + } + } + } +} diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/IFontHandleManager.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/IFontHandleManager.cs new file mode 100644 index 000000000..93c688608 --- /dev/null +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/IFontHandleManager.cs @@ -0,0 +1,32 @@ +namespace Dalamud.Interface.ManagedFontAtlas.Internals; + +/// +/// Manager for . +/// +internal interface IFontHandleManager : IDisposable +{ + /// + event Action? RebuildRecommend; + + /// + /// Gets the name of the font handle manager. For logging and debugging purposes. + /// + string Name { get; } + + /// + /// Gets or sets the active font handle substance. + /// + IFontHandleSubstance? Substance { get; set; } + + /// + /// Decrease font reference counter. + /// + /// Handle being released. + void FreeFontHandle(IFontHandle handle); + + /// + /// Creates a new substance of the font atlas. + /// + /// The new substance. + IFontHandleSubstance NewSubstance(); +} diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/IFontHandleSubstance.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/IFontHandleSubstance.cs new file mode 100644 index 000000000..f6c5c6591 --- /dev/null +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/IFontHandleSubstance.cs @@ -0,0 +1,54 @@ +using ImGuiNET; + +namespace Dalamud.Interface.ManagedFontAtlas.Internals; + +/// +/// Substance of a font. +/// +internal interface IFontHandleSubstance : IDisposable +{ + /// + /// Gets the manager relevant to this instance of . + /// + IFontHandleManager Manager { get; } + + /// + /// Gets the font. + /// + /// The handle to get from. + /// Corresponding font or null. + ImFontPtr GetFontPtr(IFontHandle handle); + + /// + /// Gets the exception happened while loading for the font. + /// + /// The handle to get from. + /// Corresponding font or null. + Exception? GetBuildException(IFontHandle handle); + + /// + /// Called before call. + /// + /// The toolkit. + void OnPreBuild(IFontAtlasBuildToolkitPreBuild toolkitPreBuild); + + /// + /// Called between and calls.
+ /// Any further modification to will result in undefined behavior. + ///
+ /// The toolkit. + void OnPreBuildCleanup(IFontAtlasBuildToolkitPreBuild toolkitPreBuild); + + /// + /// Called after call. + /// + /// The toolkit. + void OnPostBuild(IFontAtlasBuildToolkitPostBuild toolkitPostBuild); + + /// + /// Called on the specific thread depending on after + /// promoting the staging atlas to direct use with . + /// + /// The toolkit. + void OnPostPromotion(IFontAtlasBuildToolkitPostPromotion toolkitPostPromotion); +} diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.Common.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.Common.cs new file mode 100644 index 000000000..8e7149853 --- /dev/null +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.Common.cs @@ -0,0 +1,203 @@ +using System.Buffers.Binary; +using System.Runtime.InteropServices; +using System.Text; + +namespace Dalamud.Interface.ManagedFontAtlas.Internals; + +/// +/// Deals with TrueType. +/// +internal static partial class TrueTypeUtils +{ + private struct Fixed : IComparable + { + public ushort Major; + public ushort Minor; + + public Fixed(ushort major, ushort minor) + { + this.Major = major; + this.Minor = minor; + } + + public Fixed(PointerSpan span) + { + var offset = 0; + span.ReadBig(ref offset, out this.Major); + span.ReadBig(ref offset, out this.Minor); + } + + public int CompareTo(Fixed other) + { + var majorComparison = this.Major.CompareTo(other.Major); + return majorComparison != 0 ? majorComparison : this.Minor.CompareTo(other.Minor); + } + } + + private struct KerningPair : IEquatable + { + public ushort Left; + public ushort Right; + public short Value; + + public KerningPair(PointerSpan span) + { + var offset = 0; + span.ReadBig(ref offset, out this.Left); + span.ReadBig(ref offset, out this.Right); + span.ReadBig(ref offset, out this.Value); + } + + public KerningPair(ushort left, ushort right, short value) + { + this.Left = left; + this.Right = right; + this.Value = value; + } + + public static bool operator ==(KerningPair left, KerningPair right) => left.Equals(right); + + public static bool operator !=(KerningPair left, KerningPair right) => !left.Equals(right); + + public static KerningPair ReverseEndianness(KerningPair pair) => new() + { + Left = BinaryPrimitives.ReverseEndianness(pair.Left), + Right = BinaryPrimitives.ReverseEndianness(pair.Right), + Value = BinaryPrimitives.ReverseEndianness(pair.Value), + }; + + public bool Equals(KerningPair other) => + this.Left == other.Left && this.Right == other.Right && this.Value == other.Value; + + public override bool Equals(object? obj) => obj is KerningPair other && this.Equals(other); + + public override int GetHashCode() => HashCode.Combine(this.Left, this.Right, this.Value); + + public override string ToString() => $"KerningPair[{this.Left}, {this.Right}] = {this.Value}"; + } + + [StructLayout(LayoutKind.Explicit, Size = 4)] + private struct PlatformAndEncoding + { + [FieldOffset(0)] + public PlatformId Platform; + + [FieldOffset(2)] + public UnicodeEncodingId UnicodeEncoding; + + [FieldOffset(2)] + public MacintoshEncodingId MacintoshEncoding; + + [FieldOffset(2)] + public IsoEncodingId IsoEncoding; + + [FieldOffset(2)] + public WindowsEncodingId WindowsEncoding; + + public PlatformAndEncoding(PointerSpan source) + { + var offset = 0; + source.ReadBig(ref offset, out this.Platform); + source.ReadBig(ref offset, out this.UnicodeEncoding); + } + + public static PlatformAndEncoding ReverseEndianness(PlatformAndEncoding value) => new() + { + Platform = (PlatformId)BinaryPrimitives.ReverseEndianness((ushort)value.Platform), + UnicodeEncoding = (UnicodeEncodingId)BinaryPrimitives.ReverseEndianness((ushort)value.UnicodeEncoding), + }; + + public readonly string Decode(Span data) + { + switch (this.Platform) + { + case PlatformId.Unicode: + switch (this.UnicodeEncoding) + { + case UnicodeEncodingId.Unicode_2_0_Bmp: + case UnicodeEncodingId.Unicode_2_0_Full: + return Encoding.BigEndianUnicode.GetString(data); + } + + break; + + case PlatformId.Macintosh: + switch (this.MacintoshEncoding) + { + case MacintoshEncodingId.Roman: + return Encoding.ASCII.GetString(data); + } + + break; + + case PlatformId.Windows: + switch (this.WindowsEncoding) + { + case WindowsEncodingId.Symbol: + case WindowsEncodingId.UnicodeBmp: + case WindowsEncodingId.UnicodeFullRepertoire: + return Encoding.BigEndianUnicode.GetString(data); + } + + break; + } + + throw new NotSupportedException(); + } + } + + [StructLayout(LayoutKind.Explicit)] + private struct TagStruct : IEquatable, IComparable + { + [FieldOffset(0)] + public unsafe fixed byte Tag[4]; + + [FieldOffset(0)] + public uint NativeValue; + + public unsafe TagStruct(char c1, char c2, char c3, char c4) + { + this.Tag[0] = checked((byte)c1); + this.Tag[1] = checked((byte)c2); + this.Tag[2] = checked((byte)c3); + this.Tag[3] = checked((byte)c4); + } + + public unsafe TagStruct(PointerSpan span) + { + this.Tag[0] = span[0]; + this.Tag[1] = span[1]; + this.Tag[2] = span[2]; + this.Tag[3] = span[3]; + } + + public unsafe TagStruct(ReadOnlySpan span) + { + this.Tag[0] = span[0]; + this.Tag[1] = span[1]; + this.Tag[2] = span[2]; + this.Tag[3] = span[3]; + } + + public unsafe byte this[int index] + { + get => this.Tag[index]; + set => this.Tag[index] = value; + } + + public static bool operator ==(TagStruct left, TagStruct right) => left.Equals(right); + + public static bool operator !=(TagStruct left, TagStruct right) => !left.Equals(right); + + public bool Equals(TagStruct other) => this.NativeValue == other.NativeValue; + + public override bool Equals(object? obj) => obj is TagStruct other && this.Equals(other); + + public override int GetHashCode() => (int)this.NativeValue; + + public int CompareTo(TagStruct other) => this.NativeValue.CompareTo(other.NativeValue); + + public override unsafe string ToString() => + $"0x{this.NativeValue:08X} \"{(char)this.Tag[0]}{(char)this.Tag[1]}{(char)this.Tag[2]}{(char)this.Tag[3]}\""; + } +} diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.Enums.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.Enums.cs new file mode 100644 index 000000000..f6a653a51 --- /dev/null +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.Enums.cs @@ -0,0 +1,84 @@ +using System.Diagnostics.CodeAnalysis; + +namespace Dalamud.Interface.ManagedFontAtlas.Internals; + +/// +/// Deals with TrueType. +/// +internal static partial class TrueTypeUtils +{ + [SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Version name in enum value names")] + private enum IsoEncodingId : ushort + { + Ascii = 0, + Iso_10646 = 1, + Iso_8859_1 = 2, + } + + private enum MacintoshEncodingId : ushort + { + Roman = 0, + } + + private enum NameId : ushort + { + CopyrightNotice = 0, + FamilyName = 1, + SubfamilyName = 2, + UniqueId = 3, + FullFontName = 4, + VersionString = 5, + PostScriptName = 6, + Trademark = 7, + Manufacturer = 8, + Designer = 9, + Description = 10, + UrlVendor = 11, + UrlDesigner = 12, + LicenseDescription = 13, + LicenseInfoUrl = 14, + TypographicFamilyName = 16, + TypographicSubfamilyName = 17, + CompatibleFullMac = 18, + SampleText = 19, + PoscSriptCidFindFontName = 20, + WwsFamilyName = 21, + WwsSubfamilyName = 22, + LightBackgroundPalette = 23, + DarkBackgroundPalette = 24, + VariationPostScriptNamePrefix = 25, + } + + private enum PlatformId : ushort + { + Unicode = 0, + Macintosh = 1, // discouraged + Iso = 2, // deprecated + Windows = 3, + Custom = 4, // OTF Windows NT compatibility mapping + } + + [SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Version name in enum value names")] + private enum UnicodeEncodingId : ushort + { + Unicode_1_0 = 0, // deprecated + Unicode_1_1 = 1, // deprecated + IsoIec_10646 = 2, // deprecated + Unicode_2_0_Bmp = 3, + Unicode_2_0_Full = 4, + UnicodeVariationSequences = 5, + UnicodeFullRepertoire = 6, + } + + private enum WindowsEncodingId : ushort + { + Symbol = 0, + UnicodeBmp = 1, + ShiftJis = 2, + Prc = 3, + Big5 = 4, + Wansung = 5, + Johab = 6, + UnicodeFullRepertoire = 10, + } +} diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.Files.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.Files.cs new file mode 100644 index 000000000..3d89dd806 --- /dev/null +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.Files.cs @@ -0,0 +1,148 @@ +using System.Buffers.Binary; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Runtime.CompilerServices; + +namespace Dalamud.Interface.ManagedFontAtlas.Internals; + +/// +/// Deals with TrueType. +/// +[SuppressMessage("ReSharper", "NotAccessedField.Local", Justification = "TrueType specification defined fields")] +[SuppressMessage("ReSharper", "UnusedType.Local", Justification = "TrueType specification defined types")] +[SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1401:Fields should be private", Justification = "Internal")] +[SuppressMessage( + "StyleCop.CSharp.NamingRules", + "SA1310:Field names should not contain underscore", + Justification = "Version name")] +[SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Version name")] +internal static partial class TrueTypeUtils +{ + private readonly struct SfntFile : IReadOnlyDictionary> + { + // http://formats.kaitai.io/ttf/ttf.svg + + public static readonly TagStruct FileTagTrueType1 = new('1', '\0', '\0', '\0'); + public static readonly TagStruct FileTagType1 = new('t', 'y', 'p', '1'); + public static readonly TagStruct FileTagOpenTypeWithCff = new('O', 'T', 'T', 'O'); + public static readonly TagStruct FileTagOpenType1_0 = new('\0', '\x01', '\0', '\0'); + public static readonly TagStruct FileTagTrueTypeApple = new('t', 'r', 'u', 'e'); + + public readonly PointerSpan Memory; + public readonly int OffsetInCollection; + public readonly ushort TableCount; + + public SfntFile(PointerSpan memory, int offsetInCollection = 0) + { + var span = memory.Span; + this.Memory = memory; + this.OffsetInCollection = offsetInCollection; + this.TableCount = BinaryPrimitives.ReadUInt16BigEndian(span[4..]); + } + + public int Count => this.TableCount; + + public IEnumerable Keys => this.Select(x => x.Key); + + public IEnumerable> Values => this.Select(x => x.Value); + + public PointerSpan this[TagStruct key] => this.First(x => x.Key == key).Value; + + public IEnumerator>> GetEnumerator() + { + var offset = 12; + for (var i = 0; i < this.TableCount; i++) + { + var dte = new DirectoryTableEntry(this.Memory[offset..]); + yield return new(dte.Tag, this.Memory.Slice(dte.Offset - this.OffsetInCollection, dte.Length)); + + offset += Unsafe.SizeOf(); + } + } + + IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator(); + + public bool ContainsKey(TagStruct key) => this.Any(x => x.Key == key); + + public bool TryGetValue(TagStruct key, out PointerSpan value) + { + foreach (var (k, v) in this) + { + if (k == key) + { + value = v; + return true; + } + } + + value = default; + return false; + } + + public readonly struct DirectoryTableEntry + { + public readonly PointerSpan Memory; + + public DirectoryTableEntry(PointerSpan span) => this.Memory = span; + + public TagStruct Tag => new(this.Memory); + + public uint Checksum => this.Memory.ReadU32Big(4); + + public int Offset => this.Memory.ReadI32Big(8); + + public int Length => this.Memory.ReadI32Big(12); + } + } + + private readonly struct TtcFile : IReadOnlyList + { + public static readonly TagStruct FileTag = new('t', 't', 'c', 'f'); + + public readonly PointerSpan Memory; + public readonly TagStruct Tag; + public readonly ushort MajorVersion; + public readonly ushort MinorVersion; + public readonly int FontCount; + + public TtcFile(PointerSpan memory) + { + var span = memory.Span; + this.Memory = memory; + this.Tag = new(span); + if (this.Tag != FileTag) + throw new InvalidOperationException(); + + this.MajorVersion = BinaryPrimitives.ReadUInt16BigEndian(span[4..]); + this.MinorVersion = BinaryPrimitives.ReadUInt16BigEndian(span[6..]); + this.FontCount = BinaryPrimitives.ReadInt32BigEndian(span[8..]); + } + + public int Count => this.FontCount; + + public SfntFile this[int index] + { + get + { + if (index < 0 || index >= this.FontCount) + { + throw new IndexOutOfRangeException( + $"The requested font #{index} does not exist in this .ttc file."); + } + + var offset = BinaryPrimitives.ReadInt32BigEndian(this.Memory.Span[(12 + 4 * index)..]); + return new(this.Memory[offset..], offset); + } + } + + public IEnumerator GetEnumerator() + { + for (var i = 0; i < this.FontCount; i++) + yield return this[i]; + } + + IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator(); + } +} diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.GposGsub.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.GposGsub.cs new file mode 100644 index 000000000..d200de47b --- /dev/null +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.GposGsub.cs @@ -0,0 +1,259 @@ +using System.Buffers.Binary; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics.Contracts; +using System.Linq; + +namespace Dalamud.Interface.ManagedFontAtlas.Internals; + +/// +/// Deals with TrueType. +/// +internal static partial class TrueTypeUtils +{ + [Flags] + private enum LookupFlags : byte + { + RightToLeft = 1 << 0, + IgnoreBaseGlyphs = 1 << 1, + IgnoreLigatures = 1 << 2, + IgnoreMarks = 1 << 3, + UseMarkFilteringSet = 1 << 4, + } + + private enum LookupType : ushort + { + SingleAdjustment = 1, + PairAdjustment = 2, + CursiveAttachment = 3, + MarkToBaseAttachment = 4, + MarkToLigatureAttachment = 5, + MarkToMarkAttachment = 6, + ContextPositioning = 7, + ChainedContextPositioning = 8, + ExtensionPositioning = 9, + } + + private readonly struct ClassDefTable + { + public readonly PointerSpan Memory; + + public ClassDefTable(PointerSpan memory) => this.Memory = memory; + + public ushort Format => this.Memory.ReadU16Big(0); + + public Format1ClassArray Format1 => new(this.Memory); + + public Format2ClassRanges Format2 => new(this.Memory); + + public IEnumerable<(ushort Class, ushort GlyphId)> Enumerate() + { + switch (this.Format) + { + case 1: + { + var format1 = this.Format1; + var startId = format1.StartGlyphId; + var count = format1.GlyphCount; + var classes = format1.ClassValueArray; + for (var i = 0; i < count; i++) + yield return (classes[i], (ushort)(i + startId)); + + break; + } + + case 2: + { + foreach (var range in this.Format2.ClassValueArray) + { + var @class = range.Class; + var startId = range.StartGlyphId; + var count = range.EndGlyphId - startId + 1; + for (var i = 0; i < count; i++) + yield return (@class, (ushort)(startId + i)); + } + + break; + } + } + } + + [Pure] + public ushort GetClass(ushort glyphId) + { + switch (this.Format) + { + case 1: + { + var format1 = this.Format1; + var startId = format1.StartGlyphId; + if (startId <= glyphId && glyphId < startId + format1.GlyphCount) + return this.Format1.ClassValueArray[glyphId - startId]; + + break; + } + + case 2: + { + var rangeSpan = this.Format2.ClassValueArray; + var i = rangeSpan.BinarySearch(new Format2ClassRanges.ClassRangeRecord { EndGlyphId = glyphId }); + if (i >= 0 && rangeSpan[i].ContainsGlyph(glyphId)) + return rangeSpan[i].Class; + + break; + } + } + + return 0; + } + + public readonly struct Format1ClassArray + { + public readonly PointerSpan Memory; + + public Format1ClassArray(PointerSpan memory) => this.Memory = memory; + + public ushort Format => this.Memory.ReadU16Big(0); + + public ushort StartGlyphId => this.Memory.ReadU16Big(2); + + public ushort GlyphCount => this.Memory.ReadU16Big(4); + + public BigEndianPointerSpan ClassValueArray => new( + this.Memory[6..].As(this.GlyphCount), + BinaryPrimitives.ReverseEndianness); + } + + public readonly struct Format2ClassRanges + { + public readonly PointerSpan Memory; + + public Format2ClassRanges(PointerSpan memory) => this.Memory = memory; + + public ushort ClassRangeCount => this.Memory.ReadU16Big(2); + + public BigEndianPointerSpan ClassValueArray => new( + this.Memory[4..].As(this.ClassRangeCount), + ClassRangeRecord.ReverseEndianness); + + public struct ClassRangeRecord : IComparable + { + public ushort StartGlyphId; + public ushort EndGlyphId; + public ushort Class; + + public static ClassRangeRecord ReverseEndianness(ClassRangeRecord value) => new() + { + StartGlyphId = BinaryPrimitives.ReverseEndianness(value.StartGlyphId), + EndGlyphId = BinaryPrimitives.ReverseEndianness(value.EndGlyphId), + Class = BinaryPrimitives.ReverseEndianness(value.Class), + }; + + public int CompareTo(ClassRangeRecord other) => this.EndGlyphId.CompareTo(other.EndGlyphId); + + public bool ContainsGlyph(ushort glyphId) => + this.StartGlyphId <= glyphId && glyphId <= this.EndGlyphId; + } + } + } + + private readonly struct CoverageTable + { + public readonly PointerSpan Memory; + + public CoverageTable(PointerSpan memory) => this.Memory = memory; + + public enum CoverageFormat : ushort + { + Glyphs = 1, + RangeRecords = 2, + } + + public CoverageFormat Format => this.Memory.ReadEnumBig(0); + + public ushort Count => this.Memory.ReadU16Big(2); + + public BigEndianPointerSpan Glyphs => + this.Format == CoverageFormat.Glyphs + ? new(this.Memory[4..].As(this.Count), BinaryPrimitives.ReverseEndianness) + : default(BigEndianPointerSpan); + + public BigEndianPointerSpan RangeRecords => + this.Format == CoverageFormat.RangeRecords + ? new(this.Memory[4..].As(this.Count), RangeRecord.ReverseEndianness) + : default(BigEndianPointerSpan); + + public int GetCoverageIndex(ushort glyphId) + { + switch (this.Format) + { + case CoverageFormat.Glyphs: + return this.Glyphs.BinarySearch(glyphId); + + case CoverageFormat.RangeRecords: + { + var index = this.RangeRecords.BinarySearch( + (in RangeRecord record) => glyphId.CompareTo(record.EndGlyphId)); + + if (index >= 0 && this.RangeRecords[index].ContainsGlyph(glyphId)) + return index; + + return -1; + } + + default: + return -1; + } + } + + public struct RangeRecord + { + public ushort StartGlyphId; + public ushort EndGlyphId; + public ushort StartCoverageIndex; + + public static RangeRecord ReverseEndianness(RangeRecord value) => new() + { + StartGlyphId = BinaryPrimitives.ReverseEndianness(value.StartGlyphId), + EndGlyphId = BinaryPrimitives.ReverseEndianness(value.EndGlyphId), + StartCoverageIndex = BinaryPrimitives.ReverseEndianness(value.StartCoverageIndex), + }; + + public bool ContainsGlyph(ushort glyphId) => + this.StartGlyphId <= glyphId && glyphId <= this.EndGlyphId; + } + } + + private readonly struct LookupTable : IEnumerable> + { + public readonly PointerSpan Memory; + + public LookupTable(PointerSpan memory) => this.Memory = memory; + + public LookupType Type => this.Memory.ReadEnumBig(0); + + public byte MarkAttachmentType => this.Memory[2]; + + public LookupFlags Flags => (LookupFlags)this.Memory[3]; + + public ushort SubtableCount => this.Memory.ReadU16Big(4); + + public BigEndianPointerSpan SubtableOffsets => new( + this.Memory[6..].As(this.SubtableCount), + BinaryPrimitives.ReverseEndianness); + + public PointerSpan this[int index] => this.Memory[this.SubtableOffsets[this.EnsureIndex(index)] ..]; + + public IEnumerator> GetEnumerator() + { + foreach (var i in Enumerable.Range(0, this.SubtableCount)) + yield return this.Memory[this.SubtableOffsets[i] ..]; + } + + IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator(); + + private int EnsureIndex(int index) => index >= 0 && index < this.SubtableCount + ? index + : throw new IndexOutOfRangeException(); + } +} diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.PointerSpan.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.PointerSpan.cs new file mode 100644 index 000000000..c91df4ff2 --- /dev/null +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.PointerSpan.cs @@ -0,0 +1,443 @@ +using System.Buffers.Binary; +using System.Collections; +using System.Collections.Generic; +using System.Reactive.Disposables; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace Dalamud.Interface.ManagedFontAtlas.Internals; + +/// +/// Deals with TrueType. +/// +internal static partial class TrueTypeUtils +{ + private delegate int BinarySearchComparer(in T value); + + private static IDisposable CreatePointerSpan(this T[] data, out PointerSpan pointerSpan) + where T : unmanaged + { + var gchandle = GCHandle.Alloc(data, GCHandleType.Pinned); + pointerSpan = new(gchandle.AddrOfPinnedObject(), data.Length); + return Disposable.Create(() => gchandle.Free()); + } + + private static int BinarySearch(this IReadOnlyList span, in T value) + where T : unmanaged, IComparable + { + var l = 0; + var r = span.Count - 1; + while (l <= r) + { + var i = (int)(((uint)r + (uint)l) >> 1); + var c = value.CompareTo(span[i]); + switch (c) + { + case 0: + return i; + case > 0: + l = i + 1; + break; + default: + r = i - 1; + break; + } + } + + return ~l; + } + + private static int BinarySearch(this IReadOnlyList span, BinarySearchComparer comparer) + where T : unmanaged + { + var l = 0; + var r = span.Count - 1; + while (l <= r) + { + var i = (int)(((uint)r + (uint)l) >> 1); + var c = comparer(span[i]); + switch (c) + { + case 0: + return i; + case > 0: + l = i + 1; + break; + default: + r = i - 1; + break; + } + } + + return ~l; + } + + private static short ReadI16Big(this PointerSpan ps, int offset) => + BinaryPrimitives.ReadInt16BigEndian(ps.Span[offset..]); + + private static int ReadI32Big(this PointerSpan ps, int offset) => + BinaryPrimitives.ReadInt32BigEndian(ps.Span[offset..]); + + private static long ReadI64Big(this PointerSpan ps, int offset) => + BinaryPrimitives.ReadInt64BigEndian(ps.Span[offset..]); + + private static ushort ReadU16Big(this PointerSpan ps, int offset) => + BinaryPrimitives.ReadUInt16BigEndian(ps.Span[offset..]); + + private static uint ReadU32Big(this PointerSpan ps, int offset) => + BinaryPrimitives.ReadUInt32BigEndian(ps.Span[offset..]); + + private static ulong ReadU64Big(this PointerSpan ps, int offset) => + BinaryPrimitives.ReadUInt64BigEndian(ps.Span[offset..]); + + private static Half ReadF16Big(this PointerSpan ps, int offset) => + BinaryPrimitives.ReadHalfBigEndian(ps.Span[offset..]); + + private static float ReadF32Big(this PointerSpan ps, int offset) => + BinaryPrimitives.ReadSingleBigEndian(ps.Span[offset..]); + + private static double ReadF64Big(this PointerSpan ps, int offset) => + BinaryPrimitives.ReadDoubleBigEndian(ps.Span[offset..]); + + private static void ReadBig(this PointerSpan ps, int offset, out short value) => + value = BinaryPrimitives.ReadInt16BigEndian(ps.Span[offset..]); + + private static void ReadBig(this PointerSpan ps, int offset, out int value) => + value = BinaryPrimitives.ReadInt32BigEndian(ps.Span[offset..]); + + private static void ReadBig(this PointerSpan ps, int offset, out long value) => + value = BinaryPrimitives.ReadInt64BigEndian(ps.Span[offset..]); + + private static void ReadBig(this PointerSpan ps, int offset, out ushort value) => + value = BinaryPrimitives.ReadUInt16BigEndian(ps.Span[offset..]); + + private static void ReadBig(this PointerSpan ps, int offset, out uint value) => + value = BinaryPrimitives.ReadUInt32BigEndian(ps.Span[offset..]); + + private static void ReadBig(this PointerSpan ps, int offset, out ulong value) => + value = BinaryPrimitives.ReadUInt64BigEndian(ps.Span[offset..]); + + private static void ReadBig(this PointerSpan ps, int offset, out Half value) => + value = BinaryPrimitives.ReadHalfBigEndian(ps.Span[offset..]); + + private static void ReadBig(this PointerSpan ps, int offset, out float value) => + value = BinaryPrimitives.ReadSingleBigEndian(ps.Span[offset..]); + + private static void ReadBig(this PointerSpan ps, int offset, out double value) => + value = BinaryPrimitives.ReadDoubleBigEndian(ps.Span[offset..]); + + private static void ReadBig(this PointerSpan ps, ref int offset, out short value) + { + ps.ReadBig(offset, out value); + offset += 2; + } + + private static void ReadBig(this PointerSpan ps, ref int offset, out int value) + { + ps.ReadBig(offset, out value); + offset += 4; + } + + private static void ReadBig(this PointerSpan ps, ref int offset, out long value) + { + ps.ReadBig(offset, out value); + offset += 8; + } + + private static void ReadBig(this PointerSpan ps, ref int offset, out ushort value) + { + ps.ReadBig(offset, out value); + offset += 2; + } + + private static void ReadBig(this PointerSpan ps, ref int offset, out uint value) + { + ps.ReadBig(offset, out value); + offset += 4; + } + + private static void ReadBig(this PointerSpan ps, ref int offset, out ulong value) + { + ps.ReadBig(offset, out value); + offset += 8; + } + + private static void ReadBig(this PointerSpan ps, ref int offset, out Half value) + { + ps.ReadBig(offset, out value); + offset += 2; + } + + private static void ReadBig(this PointerSpan ps, ref int offset, out float value) + { + ps.ReadBig(offset, out value); + offset += 4; + } + + private static void ReadBig(this PointerSpan ps, ref int offset, out double value) + { + ps.ReadBig(offset, out value); + offset += 8; + } + + private static unsafe T ReadEnumBig(this PointerSpan ps, int offset) where T : unmanaged, Enum + { + switch (Marshal.SizeOf(Enum.GetUnderlyingType(typeof(T)))) + { + case 1: + var b1 = ps.Span[offset]; + return *(T*)&b1; + case 2: + var b2 = ps.ReadU16Big(offset); + return *(T*)&b2; + case 4: + var b4 = ps.ReadU32Big(offset); + return *(T*)&b4; + case 8: + var b8 = ps.ReadU64Big(offset); + return *(T*)&b8; + default: + throw new ArgumentException("Enum is not of size 1, 2, 4, or 8.", nameof(T), null); + } + } + + private static void ReadBig(this PointerSpan ps, int offset, out T value) where T : unmanaged, Enum => + value = ps.ReadEnumBig(offset); + + private static void ReadBig(this PointerSpan ps, ref int offset, out T value) where T : unmanaged, Enum + { + value = ps.ReadEnumBig(offset); + offset += Unsafe.SizeOf(); + } + + private readonly unsafe struct PointerSpan : IList, IReadOnlyList, ICollection + where T : unmanaged + { + public readonly T* Pointer; + + public PointerSpan(T* pointer, int count) + { + this.Pointer = pointer; + this.Count = count; + } + + public PointerSpan(nint pointer, int count) + : this((T*)pointer, count) + { + } + + public Span Span => new(this.Pointer, this.Count); + + public bool IsEmpty => this.Count == 0; + + public int Count { get; } + + public int Length => this.Count; + + public int ByteCount => sizeof(T) * this.Count; + + bool ICollection.IsSynchronized => false; + + object ICollection.SyncRoot => this; + + bool ICollection.IsReadOnly => false; + + public ref T this[int index] => ref this.Pointer[this.EnsureIndex(index)]; + + public PointerSpan this[Range range] => this.Slice(range.GetOffsetAndLength(this.Count)); + + T IList.this[int index] + { + get => this.Pointer[this.EnsureIndex(index)]; + set => this.Pointer[this.EnsureIndex(index)] = value; + } + + T IReadOnlyList.this[int index] => this.Pointer[this.EnsureIndex(index)]; + + public bool ContainsPointer(T2* obj) where T2 : unmanaged => + (T*)obj >= this.Pointer && (T*)(obj + 1) <= this.Pointer + this.Count; + + public PointerSpan Slice(int offset, int count) => new(this.Pointer + offset, count); + + public PointerSpan Slice((int Offset, int Count) offsetAndCount) + => this.Slice(offsetAndCount.Offset, offsetAndCount.Count); + + public PointerSpan As(int count) + where T2 : unmanaged => + count > this.Count / sizeof(T2) + ? throw new ArgumentOutOfRangeException( + nameof(count), + count, + $"Wanted {count} items; had {this.Count / sizeof(T2)} items") + : new((T2*)this.Pointer, count); + + public PointerSpan As() + where T2 : unmanaged => + new((T2*)this.Pointer, this.Count / sizeof(T2)); + + public IEnumerator GetEnumerator() + { + for (var i = 0; i < this.Count; i++) + yield return this[i]; + } + + void ICollection.Add(T item) => throw new NotSupportedException(); + + void ICollection.Clear() => throw new NotSupportedException(); + + bool ICollection.Contains(T item) + { + for (var i = 0; i < this.Count; i++) + { + if (Equals(this.Pointer[i], item)) + return true; + } + + return false; + } + + void ICollection.CopyTo(T[] array, int arrayIndex) + { + if (array.Length < this.Count) + throw new ArgumentException(null, nameof(array)); + + if (array.Length < arrayIndex + this.Count) + throw new ArgumentException(null, nameof(arrayIndex)); + + for (var i = 0; i < this.Count; i++) + array[arrayIndex + i] = this.Pointer[i]; + } + + bool ICollection.Remove(T item) => throw new NotSupportedException(); + + int IList.IndexOf(T item) + { + for (var i = 0; i < this.Count; i++) + { + if (Equals(this.Pointer[i], item)) + return i; + } + + return -1; + } + + void IList.Insert(int index, T item) => throw new NotSupportedException(); + + void IList.RemoveAt(int index) => throw new NotSupportedException(); + + void ICollection.CopyTo(Array array, int arrayIndex) + { + if (array.Length < this.Count) + throw new ArgumentException(null, nameof(array)); + + if (array.Length < arrayIndex + this.Count) + throw new ArgumentException(null, nameof(arrayIndex)); + + for (var i = 0; i < this.Count; i++) + array.SetValue(this.Pointer[i], arrayIndex + i); + } + + IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator(); + + private int EnsureIndex(int index) => + index >= 0 && index < this.Count ? index : throw new IndexOutOfRangeException(); + } + + private readonly unsafe struct BigEndianPointerSpan + : IList, IReadOnlyList, ICollection + where T : unmanaged + { + public readonly T* Pointer; + + private readonly Func reverseEndianness; + + public BigEndianPointerSpan(PointerSpan pointerSpan, Func reverseEndianness) + { + this.reverseEndianness = reverseEndianness; + this.Pointer = pointerSpan.Pointer; + this.Count = pointerSpan.Count; + } + + public int Count { get; } + + public int Length => this.Count; + + public int ByteCount => sizeof(T) * this.Count; + + public bool IsSynchronized => true; + + public object SyncRoot => this; + + public bool IsReadOnly => true; + + public T this[int index] + { + get => + BitConverter.IsLittleEndian + ? this.reverseEndianness(this.Pointer[this.EnsureIndex(index)]) + : this.Pointer[this.EnsureIndex(index)]; + set => this.Pointer[this.EnsureIndex(index)] = + BitConverter.IsLittleEndian + ? this.reverseEndianness(value) + : value; + } + + public IEnumerator GetEnumerator() + { + for (var i = 0; i < this.Count; i++) + yield return this[i]; + } + + void ICollection.Add(T item) => throw new NotSupportedException(); + + void ICollection.Clear() => throw new NotSupportedException(); + + bool ICollection.Contains(T item) => throw new NotSupportedException(); + + void ICollection.CopyTo(T[] array, int arrayIndex) + { + if (array.Length < this.Count) + throw new ArgumentException(null, nameof(array)); + + if (array.Length < arrayIndex + this.Count) + throw new ArgumentException(null, nameof(arrayIndex)); + + for (var i = 0; i < this.Count; i++) + array[arrayIndex + i] = this[i]; + } + + bool ICollection.Remove(T item) => throw new NotSupportedException(); + + int IList.IndexOf(T item) + { + for (var i = 0; i < this.Count; i++) + { + if (Equals(this[i], item)) + return i; + } + + return -1; + } + + void IList.Insert(int index, T item) => throw new NotSupportedException(); + + void IList.RemoveAt(int index) => throw new NotSupportedException(); + + void ICollection.CopyTo(Array array, int arrayIndex) + { + if (array.Length < this.Count) + throw new ArgumentException(null, nameof(array)); + + if (array.Length < arrayIndex + this.Count) + throw new ArgumentException(null, nameof(arrayIndex)); + + for (var i = 0; i < this.Count; i++) + array.SetValue(this[i], arrayIndex + i); + } + + IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator(); + + private int EnsureIndex(int index) => + index >= 0 && index < this.Count ? index : throw new IndexOutOfRangeException(); + } +} diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.Tables.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.Tables.cs new file mode 100644 index 000000000..80cf4b7da --- /dev/null +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.Tables.cs @@ -0,0 +1,1391 @@ +using System.Buffers.Binary; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Text; + +namespace Dalamud.Interface.ManagedFontAtlas.Internals; + +/// +/// Deals with TrueType. +/// +[SuppressMessage("ReSharper", "NotAccessedField.Local", Justification = "TrueType specification defined fields")] +[SuppressMessage("ReSharper", "UnusedType.Local", Justification = "TrueType specification defined types")] +[SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1401:Fields should be private", Justification = "Internal")] +internal static partial class TrueTypeUtils +{ + [Flags] + private enum ValueFormat : ushort + { + PlacementX = 1 << 0, + PlacementY = 1 << 1, + AdvanceX = 1 << 2, + AdvanceY = 1 << 3, + PlacementDeviceOffsetX = 1 << 4, + PlacementDeviceOffsetY = 1 << 5, + AdvanceDeviceOffsetX = 1 << 6, + AdvanceDeviceOffsetY = 1 << 7, + + ValidBits = 0 + | PlacementX | PlacementY + | AdvanceX | AdvanceY + | PlacementDeviceOffsetX | PlacementDeviceOffsetY + | AdvanceDeviceOffsetX | AdvanceDeviceOffsetY, + } + + private static int NumBytes(this ValueFormat value) => + ushort.PopCount((ushort)(value & ValueFormat.ValidBits)) * 2; + + private readonly struct Cmap + { + // https://docs.microsoft.com/en-us/typography/opentype/spec/cmap + // https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6cmap.html + + public static readonly TagStruct DirectoryTableTag = new('c', 'm', 'a', 'p'); + + public readonly PointerSpan Memory; + + public Cmap(SfntFile file) + : this(file[DirectoryTableTag]) + { + } + + public Cmap(PointerSpan memory) => this.Memory = memory; + + public ushort Version => this.Memory.ReadU16Big(0); + + public ushort RecordCount => this.Memory.ReadU16Big(2); + + public BigEndianPointerSpan Records => new( + this.Memory[4..].As(this.RecordCount), + EncodingRecord.ReverseEndianness); + + public EncodingRecord? UnicodeEncodingRecord => + this.Records.Select(x => (EncodingRecord?)x).FirstOrDefault( + x => x!.Value.PlatformAndEncoding is + { Platform: PlatformId.Unicode, UnicodeEncoding: UnicodeEncodingId.Unicode_2_0_Bmp }) + ?? + this.Records.Select(x => (EncodingRecord?)x).FirstOrDefault( + x => x!.Value.PlatformAndEncoding is + { Platform: PlatformId.Unicode, UnicodeEncoding: UnicodeEncodingId.Unicode_2_0_Full }) + ?? + this.Records.Select(x => (EncodingRecord?)x).FirstOrDefault( + x => x!.Value.PlatformAndEncoding is + { Platform: PlatformId.Unicode, UnicodeEncoding: UnicodeEncodingId.UnicodeFullRepertoire }) + ?? + this.Records.Select(x => (EncodingRecord?)x).FirstOrDefault( + x => x!.Value.PlatformAndEncoding is + { Platform: PlatformId.Windows, WindowsEncoding: WindowsEncodingId.UnicodeBmp }) + ?? + this.Records.Select(x => (EncodingRecord?)x).FirstOrDefault( + x => x!.Value.PlatformAndEncoding is + { Platform: PlatformId.Windows, WindowsEncoding: WindowsEncodingId.UnicodeFullRepertoire }); + + public CmapFormat? UnicodeTable => this.GetTable(this.UnicodeEncodingRecord); + + public CmapFormat? GetTable(EncodingRecord? encodingRecord) => + encodingRecord is { } record + ? this.Memory.ReadU16Big(record.SubtableOffset) switch + { + 0 => new CmapFormat0(this.Memory[record.SubtableOffset..]), + 2 => new CmapFormat2(this.Memory[record.SubtableOffset..]), + 4 => new CmapFormat4(this.Memory[record.SubtableOffset..]), + 6 => new CmapFormat6(this.Memory[record.SubtableOffset..]), + 8 => new CmapFormat8(this.Memory[record.SubtableOffset..]), + 10 => new CmapFormat10(this.Memory[record.SubtableOffset..]), + 12 or 13 => new CmapFormat12And13(this.Memory[record.SubtableOffset..]), + _ => null, + } + : null; + + public struct EncodingRecord + { + public PlatformAndEncoding PlatformAndEncoding; + public int SubtableOffset; + + public EncodingRecord(PointerSpan span) + { + this.PlatformAndEncoding = new(span); + var offset = Unsafe.SizeOf(); + span.ReadBig(ref offset, out this.SubtableOffset); + } + + public static EncodingRecord ReverseEndianness(EncodingRecord value) => new() + { + PlatformAndEncoding = PlatformAndEncoding.ReverseEndianness(value.PlatformAndEncoding), + SubtableOffset = BinaryPrimitives.ReverseEndianness(value.SubtableOffset), + }; + } + + public struct MapGroup : IComparable + { + public int StartCharCode; + public int EndCharCode; + public int GlyphId; + + public MapGroup(PointerSpan span) + { + var offset = 0; + span.ReadBig(ref offset, out this.StartCharCode); + span.ReadBig(ref offset, out this.EndCharCode); + span.ReadBig(ref offset, out this.GlyphId); + } + + public static MapGroup ReverseEndianness(MapGroup obj) => new() + { + StartCharCode = BinaryPrimitives.ReverseEndianness(obj.StartCharCode), + EndCharCode = BinaryPrimitives.ReverseEndianness(obj.EndCharCode), + GlyphId = BinaryPrimitives.ReverseEndianness(obj.GlyphId), + }; + + public int CompareTo(MapGroup other) + { + var endCharCodeComparison = this.EndCharCode.CompareTo(other.EndCharCode); + if (endCharCodeComparison != 0) return endCharCodeComparison; + + var startCharCodeComparison = this.StartCharCode.CompareTo(other.StartCharCode); + if (startCharCodeComparison != 0) return startCharCodeComparison; + + return this.GlyphId.CompareTo(other.GlyphId); + } + } + + public abstract class CmapFormat : IReadOnlyDictionary + { + public int Count => this.Count(x => x.Value != 0); + + public IEnumerable Keys => this.Select(x => x.Key); + + public IEnumerable Values => this.Select(x => x.Value); + + public ushort this[int key] => throw new NotImplementedException(); + + public abstract ushort CharToGlyph(int c); + + public abstract IEnumerator> GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator(); + + public bool ContainsKey(int key) => this.CharToGlyph(key) != 0; + + public bool TryGetValue(int key, out ushort value) + { + value = this.CharToGlyph(key); + return value != 0; + } + } + + public class CmapFormat0 : CmapFormat + { + public readonly PointerSpan Memory; + + public CmapFormat0(PointerSpan memory) => this.Memory = memory; + + public ushort Format => this.Memory.ReadU16Big(0); + + public ushort Length => this.Memory.ReadU16Big(2); + + public ushort Language => this.Memory.ReadU16Big(4); + + public PointerSpan GlyphIdArray => this.Memory.Slice(6, 256); + + public override ushort CharToGlyph(int c) => c is >= 0 and < 256 ? this.GlyphIdArray[c] : (byte)0; + + public override IEnumerator> GetEnumerator() + { + for (var codepoint = 0; codepoint < 256; codepoint++) + { + if (this.GlyphIdArray[codepoint] is var glyphId and not 0) + yield return new(codepoint, glyphId); + } + } + } + + public class CmapFormat2 : CmapFormat + { + public readonly PointerSpan Memory; + + public CmapFormat2(PointerSpan memory) => this.Memory = memory; + + public ushort Format => this.Memory.ReadU16Big(0); + + public ushort Length => this.Memory.ReadU16Big(2); + + public ushort Language => this.Memory.ReadU16Big(4); + + public BigEndianPointerSpan SubHeaderKeys => new( + this.Memory[6..].As(256), + BinaryPrimitives.ReverseEndianness); + + public PointerSpan Data => this.Memory[518..]; + + public bool TryGetSubHeader( + int keyIndex, out SubHeader subheader, out BigEndianPointerSpan glyphSpan) + { + if (keyIndex < 0 || keyIndex >= this.SubHeaderKeys.Count) + { + subheader = default; + glyphSpan = default; + return false; + } + + var offset = this.SubHeaderKeys[keyIndex]; + if (offset + Unsafe.SizeOf() > this.Data.Length) + { + subheader = default; + glyphSpan = default; + return false; + } + + subheader = new(this.Data[offset..]); + glyphSpan = new( + this.Data[(offset + Unsafe.SizeOf() + subheader.IdRangeOffset)..] + .As(subheader.EntryCount), + BinaryPrimitives.ReverseEndianness); + + return true; + } + + public override ushort CharToGlyph(int c) + { + if (!this.TryGetSubHeader(c >> 8, out var sh, out var glyphSpan)) + return 0; + + c = (c & 0xFF) - sh.FirstCode; + if (c > 0 || c >= glyphSpan.Count) + return 0; + + var res = glyphSpan[c]; + return res == 0 ? (ushort)0 : unchecked((ushort)(res + sh.IdDelta)); + } + + public override IEnumerator> GetEnumerator() + { + for (var i = 0; i < this.SubHeaderKeys.Count; i++) + { + if (!this.TryGetSubHeader(i, out var sh, out var glyphSpan)) + continue; + + for (var j = 0; j < glyphSpan.Count; j++) + { + var res = glyphSpan[j]; + if (res == 0) + continue; + + var glyphId = unchecked((ushort)(res + sh.IdDelta)); + if (glyphId == 0) + continue; + + var codepoint = (i << 8) | (sh.FirstCode + j); + yield return new(codepoint, glyphId); + } + } + } + + public struct SubHeader + { + public ushort FirstCode; + public ushort EntryCount; + public ushort IdDelta; + public ushort IdRangeOffset; + + public SubHeader(PointerSpan span) + { + var offset = 0; + span.ReadBig(ref offset, out this.FirstCode); + span.ReadBig(ref offset, out this.EntryCount); + span.ReadBig(ref offset, out this.IdDelta); + span.ReadBig(ref offset, out this.IdRangeOffset); + } + } + } + + public class CmapFormat4 : CmapFormat + { + public const int EndCodesOffset = 14; + + public readonly PointerSpan Memory; + + public CmapFormat4(PointerSpan memory) => this.Memory = memory; + + public ushort Format => this.Memory.ReadU16Big(0); + + public ushort Length => this.Memory.ReadU16Big(2); + + public ushort Language => this.Memory.ReadU16Big(4); + + public ushort SegCountX2 => this.Memory.ReadU16Big(6); + + public ushort SearchRange => this.Memory.ReadU16Big(8); + + public ushort EntrySelector => this.Memory.ReadU16Big(10); + + public ushort RangeShift => this.Memory.ReadU16Big(12); + + public BigEndianPointerSpan EndCodes => new( + this.Memory.Slice(EndCodesOffset, this.SegCountX2).As(), + BinaryPrimitives.ReverseEndianness); + + public BigEndianPointerSpan StartCodes => new( + this.Memory.Slice(EndCodesOffset + 2 + (1 * this.SegCountX2), this.SegCountX2).As(), + BinaryPrimitives.ReverseEndianness); + + public BigEndianPointerSpan IdDeltas => new( + this.Memory.Slice(EndCodesOffset + 2 + (2 * this.SegCountX2), this.SegCountX2).As(), + BinaryPrimitives.ReverseEndianness); + + public BigEndianPointerSpan IdRangeOffsets => new( + this.Memory.Slice(EndCodesOffset + 2 + (3 * this.SegCountX2), this.SegCountX2).As(), + BinaryPrimitives.ReverseEndianness); + + public BigEndianPointerSpan GlyphIds => new( + this.Memory.Slice(EndCodesOffset + 2 + (4 * this.SegCountX2), this.SegCountX2).As(), + BinaryPrimitives.ReverseEndianness); + + public override ushort CharToGlyph(int c) + { + if (c is < 0 or >= 0x10000) + return 0; + + var i = this.EndCodes.BinarySearch((ushort)c); + if (i < 0) + return 0; + + var startCode = this.StartCodes[i]; + var endCode = this.EndCodes[i]; + if (c < startCode || c > endCode) + return 0; + + var idRangeOffset = this.IdRangeOffsets[i]; + var idDelta = this.IdDeltas[i]; + if (idRangeOffset == 0) + return unchecked((ushort)(c + idDelta)); + + var ptr = EndCodesOffset + 2 + (3 * this.SegCountX2) + i * 2 + idRangeOffset; + if (ptr > this.Memory.Length) + return 0; + + var glyphs = new BigEndianPointerSpan( + this.Memory[ptr..].As(endCode - startCode + 1), + BinaryPrimitives.ReverseEndianness); + + var glyph = glyphs[c - startCode]; + return unchecked(glyph == 0 ? (ushort)0 : (ushort)(idDelta + glyph)); + } + + public override IEnumerator> GetEnumerator() + { + var startCodes = this.StartCodes; + var endCodes = this.EndCodes; + var idDeltas = this.IdDeltas; + var idRangeOffsets = this.IdRangeOffsets; + + for (var i = 0; i < this.SegCountX2 / 2; i++) + { + var startCode = startCodes[i]; + var endCode = endCodes[i]; + var idRangeOffset = idRangeOffsets[i]; + var idDelta = idDeltas[i]; + + if (idRangeOffset == 0) + { + for (var c = (int)startCode; c <= endCode; c++) + yield return new(c, (ushort)(c + idDelta)); + } + else + { + var ptr = EndCodesOffset + 2 + (3 * this.SegCountX2) + i * 2 + idRangeOffset; + if (ptr >= this.Memory.Length) + continue; + + var glyphs = new BigEndianPointerSpan( + this.Memory[ptr..].As(endCode - startCode + 1), + BinaryPrimitives.ReverseEndianness); + + for (var j = 0; j < glyphs.Count; j++) + { + var glyphId = glyphs[j]; + if (glyphId == 0) + continue; + + glyphId += idDelta; + if (glyphId == 0) + continue; + + yield return new(startCode + j, glyphId); + } + } + } + } + } + + public class CmapFormat6 : CmapFormat + { + public readonly PointerSpan Memory; + + public CmapFormat6(PointerSpan memory) => this.Memory = memory; + + public ushort Format => this.Memory.ReadU16Big(0); + + public ushort Length => this.Memory.ReadU16Big(2); + + public ushort Language => this.Memory.ReadU16Big(4); + + public ushort FirstCode => this.Memory.ReadU16Big(6); + + public ushort EntryCount => this.Memory.ReadU16Big(8); + + public BigEndianPointerSpan GlyphIds => new( + this.Memory[10..].As(this.EntryCount), + BinaryPrimitives.ReverseEndianness); + + public override ushort CharToGlyph(int c) + { + var glyphIds = this.GlyphIds; + if (c < this.FirstCode || c >= this.FirstCode + this.GlyphIds.Count) + return 0; + + return glyphIds[c - this.FirstCode]; + } + + public override IEnumerator> GetEnumerator() + { + var glyphIds = this.GlyphIds; + for (var i = 0; i < this.GlyphIds.Length; i++) + { + var g = glyphIds[i]; + if (g != 0) + yield return new(this.FirstCode + i, g); + } + } + } + + public class CmapFormat8 : CmapFormat + { + public readonly PointerSpan Memory; + + public CmapFormat8(PointerSpan memory) => this.Memory = memory; + + public int Format => this.Memory.ReadI32Big(0); + + public int Length => this.Memory.ReadI32Big(4); + + public int Language => this.Memory.ReadI32Big(8); + + public PointerSpan Is32 => this.Memory.Slice(12, 8192); + + public int NumGroups => this.Memory.ReadI32Big(8204); + + public BigEndianPointerSpan Groups => + new(this.Memory[8208..].As(), MapGroup.ReverseEndianness); + + public override ushort CharToGlyph(int c) + { + var groups = this.Groups; + + var i = groups.BinarySearch((in MapGroup value) => c.CompareTo(value.EndCharCode)); + if (i < 0) + return 0; + + var group = groups[i]; + if (c < group.StartCharCode || c > group.EndCharCode) + return 0; + + return unchecked((ushort)(group.GlyphId + c - group.StartCharCode)); + } + + public override IEnumerator> GetEnumerator() + { + foreach (var group in this.Groups) + { + for (var j = group.StartCharCode; j <= group.EndCharCode; j++) + { + var glyphId = (ushort)(group.GlyphId + j - group.StartCharCode); + if (glyphId == 0) + continue; + + yield return new(j, glyphId); + } + } + } + } + + public class CmapFormat10 : CmapFormat + { + public readonly PointerSpan Memory; + + public CmapFormat10(PointerSpan memory) => this.Memory = memory; + + public int Format => this.Memory.ReadI32Big(0); + + public int Length => this.Memory.ReadI32Big(4); + + public int Language => this.Memory.ReadI32Big(8); + + public int StartCharCode => this.Memory.ReadI32Big(12); + + public int NumChars => this.Memory.ReadI32Big(16); + + public BigEndianPointerSpan GlyphIdArray => new( + this.Memory.Slice(20, this.NumChars * 2).As(), + BinaryPrimitives.ReverseEndianness); + + public override ushort CharToGlyph(int c) + { + if (c < this.StartCharCode || c >= this.StartCharCode + this.GlyphIdArray.Count) + return 0; + + return this.GlyphIdArray[c]; + } + + public override IEnumerator> GetEnumerator() + { + for (var i = 0; i < this.GlyphIdArray.Count; i++) + { + var glyph = this.GlyphIdArray[i]; + if (glyph != 0) + yield return new(this.StartCharCode + i, glyph); + } + } + } + + public class CmapFormat12And13 : CmapFormat + { + public readonly PointerSpan Memory; + + public CmapFormat12And13(PointerSpan memory) => this.Memory = memory; + + public ushort Format => this.Memory.ReadU16Big(0); + + public int Length => this.Memory.ReadI32Big(4); + + public int Language => this.Memory.ReadI32Big(8); + + public int NumGroups => this.Memory.ReadI32Big(12); + + public BigEndianPointerSpan Groups => new( + this.Memory[16..].As(this.NumGroups), + MapGroup.ReverseEndianness); + + public override ushort CharToGlyph(int c) + { + var groups = this.Groups; + + var i = groups.BinarySearch(new MapGroup() { EndCharCode = c }); + if (i < 0) + return 0; + + var group = groups[i]; + if (c < group.StartCharCode || c > group.EndCharCode) + return 0; + + if (this.Format == 12) + return (ushort)(group.GlyphId + c - group.StartCharCode); + else + return (ushort)group.GlyphId; + } + + public override IEnumerator> GetEnumerator() + { + var groups = this.Groups; + if (this.Format == 12) + { + foreach (var group in groups) + { + for (var j = group.StartCharCode; j <= group.EndCharCode; j++) + { + var glyphId = (ushort)(group.GlyphId + j - group.StartCharCode); + if (glyphId == 0) + continue; + + yield return new(j, glyphId); + } + } + } + else + { + foreach (var group in groups) + { + if (group.GlyphId == 0) + continue; + + for (var j = group.StartCharCode; j <= group.EndCharCode; j++) + yield return new(j, (ushort)group.GlyphId); + } + } + } + } + } + + private readonly struct Gpos + { + // https://docs.microsoft.com/en-us/typography/opentype/spec/gpos + + public static readonly TagStruct DirectoryTableTag = new('G', 'P', 'O', 'S'); + + public readonly PointerSpan Memory; + + public Gpos(SfntFile file) + : this(file[DirectoryTableTag]) + { + } + + public Gpos(PointerSpan memory) => this.Memory = memory; + + public Fixed Version => new(this.Memory); + + public ushort ScriptListOffset => this.Memory.ReadU16Big(4); + + public ushort FeatureListOffset => this.Memory.ReadU16Big(6); + + public ushort LookupListOffset => this.Memory.ReadU16Big(8); + + public uint FeatureVariationsOffset => this.Version.CompareTo(new(1, 1)) >= 0 + ? this.Memory.ReadU32Big(10) + : 0; + + public BigEndianPointerSpan LookupOffsetList => new( + this.Memory[(this.LookupListOffset + 2)..].As( + this.Memory.ReadU16Big(this.LookupListOffset)), + BinaryPrimitives.ReverseEndianness); + + public IEnumerable EnumerateLookupTables() + { + foreach (var offset in this.LookupOffsetList) + yield return new(this.Memory[(this.LookupListOffset + offset)..]); + } + + public IEnumerable ExtractAdvanceX() => + this.EnumerateLookupTables() + .SelectMany( + lookupTable => lookupTable.Type switch + { + LookupType.PairAdjustment => + lookupTable.SelectMany(y => new PairAdjustmentPositioning(y).ExtractAdvanceX()), + LookupType.ExtensionPositioning => + lookupTable + .Where(y => y.ReadU16Big(0) == 1) + .Select(y => new ExtensionPositioningSubtableFormat1(y)) + .Where(y => y.ExtensionLookupType == LookupType.PairAdjustment) + .SelectMany(y => new PairAdjustmentPositioning(y.ExtensionData).ExtractAdvanceX()), + _ => Array.Empty(), + }); + + public struct ValueRecord + { + public short PlacementX; + public short PlacementY; + public short AdvanceX; + public short AdvanceY; + public short PlacementDeviceOffsetX; + public short PlacementDeviceOffsetY; + public short AdvanceDeviceOffsetX; + public short AdvanceDeviceOffsetY; + + public ValueRecord(PointerSpan pointerSpan, ValueFormat valueFormat) + { + var offset = 0; + if ((valueFormat & ValueFormat.PlacementX) != 0) + pointerSpan.ReadBig(ref offset, out this.PlacementX); + + if ((valueFormat & ValueFormat.PlacementY) != 0) + pointerSpan.ReadBig(ref offset, out this.PlacementY); + + if ((valueFormat & ValueFormat.AdvanceX) != 0) pointerSpan.ReadBig(ref offset, out this.AdvanceX); + if ((valueFormat & ValueFormat.AdvanceY) != 0) pointerSpan.ReadBig(ref offset, out this.AdvanceY); + if ((valueFormat & ValueFormat.PlacementDeviceOffsetX) != 0) + pointerSpan.ReadBig(ref offset, out this.PlacementDeviceOffsetX); + + if ((valueFormat & ValueFormat.PlacementDeviceOffsetY) != 0) + pointerSpan.ReadBig(ref offset, out this.PlacementDeviceOffsetY); + + if ((valueFormat & ValueFormat.AdvanceDeviceOffsetX) != 0) + pointerSpan.ReadBig(ref offset, out this.AdvanceDeviceOffsetX); + + if ((valueFormat & ValueFormat.AdvanceDeviceOffsetY) != 0) + pointerSpan.ReadBig(ref offset, out this.AdvanceDeviceOffsetY); + } + } + + public readonly struct PairAdjustmentPositioning + { + public readonly PointerSpan Memory; + + public PairAdjustmentPositioning(PointerSpan memory) => this.Memory = memory; + + public ushort Format => this.Memory.ReadU16Big(0); + + public IEnumerable ExtractAdvanceX() => this.Format switch + { + 1 => new Format1(this.Memory).ExtractAdvanceX(), + 2 => new Format2(this.Memory).ExtractAdvanceX(), + _ => Array.Empty(), + }; + + public readonly struct Format1 + { + public readonly PointerSpan Memory; + + public Format1(PointerSpan memory) => this.Memory = memory; + + public ushort Format => this.Memory.ReadU16Big(0); + + public ushort CoverageOffset => this.Memory.ReadU16Big(2); + + public ValueFormat ValueFormat1 => this.Memory.ReadEnumBig(4); + + public ValueFormat ValueFormat2 => this.Memory.ReadEnumBig(6); + + public ushort PairSetCount => this.Memory.ReadU16Big(8); + + public BigEndianPointerSpan PairSetOffsets => new( + this.Memory[10..].As(this.PairSetCount), + BinaryPrimitives.ReverseEndianness); + + public CoverageTable CoverageTable => new(this.Memory[this.CoverageOffset..]); + + public PairSet this[int index] => new( + this.Memory[this.PairSetOffsets[index] ..], + this.ValueFormat1, + this.ValueFormat2); + + public IEnumerable ExtractAdvanceX() + { + if ((this.ValueFormat1 & ValueFormat.AdvanceX) == 0 && + (this.ValueFormat2 & ValueFormat.AdvanceX) == 0) + { + yield break; + } + + var coverageTable = this.CoverageTable; + switch (coverageTable.Format) + { + case CoverageTable.CoverageFormat.Glyphs: + { + var glyphSpan = coverageTable.Glyphs; + foreach (var coverageIndex in Enumerable.Range(0, glyphSpan.Count)) + { + var glyph1Id = glyphSpan[coverageIndex]; + PairSet pairSetView; + try + { + pairSetView = this[coverageIndex]; + } + catch (ArgumentOutOfRangeException) + { + yield break; + } + catch (IndexOutOfRangeException) + { + yield break; + } + + foreach (var pairIndex in Enumerable.Range(0, pairSetView.Count)) + { + var pair = pairSetView[pairIndex]; + var adj = (short)(pair.Record1.AdvanceX + pair.Record2.PlacementX); + if (adj >= 10000) + System.Diagnostics.Debugger.Break(); + + if (adj != 0) + yield return new(glyph1Id, pair.SecondGlyph, adj); + } + } + + break; + } + + case CoverageTable.CoverageFormat.RangeRecords: + { + foreach (var rangeRecord in coverageTable.RangeRecords) + { + var startGlyphId = rangeRecord.StartGlyphId; + var endGlyphId = rangeRecord.EndGlyphId; + var startCoverageIndex = rangeRecord.StartCoverageIndex; + var glyphCount = endGlyphId - startGlyphId + 1; + foreach (var glyph1Id in Enumerable.Range(startGlyphId, glyphCount)) + { + PairSet pairSetView; + try + { + pairSetView = this[startCoverageIndex + glyph1Id - startGlyphId]; + } + catch (ArgumentOutOfRangeException) + { + yield break; + } + catch (IndexOutOfRangeException) + { + yield break; + } + + foreach (var pairIndex in Enumerable.Range(0, pairSetView.Count)) + { + var pair = pairSetView[pairIndex]; + var adj = (short)(pair.Record1.AdvanceX + pair.Record2.PlacementX); + if (adj != 0) + yield return new((ushort)glyph1Id, pair.SecondGlyph, adj); + } + } + } + + break; + } + } + } + + public readonly struct PairSet + { + public readonly PointerSpan Memory; + public readonly ValueFormat ValueFormat1; + public readonly ValueFormat ValueFormat2; + public readonly int PairValue1Size; + public readonly int PairValue2Size; + public readonly int PairSize; + + public PairSet( + PointerSpan memory, + ValueFormat valueFormat1, + ValueFormat valueFormat2) + { + this.Memory = memory; + this.ValueFormat1 = valueFormat1; + this.ValueFormat2 = valueFormat2; + this.PairValue1Size = this.ValueFormat1.NumBytes(); + this.PairValue2Size = this.ValueFormat2.NumBytes(); + this.PairSize = 2 + this.PairValue1Size + this.PairValue2Size; + } + + public ushort Count => this.Memory.ReadU16Big(0); + + public PairValueRecord this[int index] + { + get + { + var pvr = this.Memory.Slice(2 + (this.PairSize * index), this.PairSize); + return new() + { + SecondGlyph = pvr.ReadU16Big(0), + Record1 = new(pvr.Slice(2, this.PairValue1Size), this.ValueFormat1), + Record2 = new( + pvr.Slice(2 + this.PairValue1Size, this.PairValue2Size), + this.ValueFormat2), + }; + } + } + + public struct PairValueRecord + { + public ushort SecondGlyph; + public ValueRecord Record1; + public ValueRecord Record2; + } + } + } + + public readonly struct Format2 + { + public readonly PointerSpan Memory; + public readonly int PairValue1Size; + public readonly int PairValue2Size; + public readonly int PairSize; + + public Format2(PointerSpan memory) + { + this.Memory = memory; + this.PairValue1Size = this.ValueFormat1.NumBytes(); + this.PairValue2Size = this.ValueFormat2.NumBytes(); + this.PairSize = this.PairValue1Size + this.PairValue2Size; + } + + public ushort Format => this.Memory.ReadU16Big(0); + + public ushort CoverageOffset => this.Memory.ReadU16Big(2); + + public ValueFormat ValueFormat1 => this.Memory.ReadEnumBig(4); + + public ValueFormat ValueFormat2 => this.Memory.ReadEnumBig(6); + + public ushort ClassDef1Offset => this.Memory.ReadU16Big(8); + + public ushort ClassDef2Offset => this.Memory.ReadU16Big(10); + + public ushort Class1Count => this.Memory.ReadU16Big(12); + + public ushort Class2Count => this.Memory.ReadU16Big(14); + + public ClassDefTable ClassDefTable1 => new(this.Memory[this.ClassDef1Offset..]); + + public ClassDefTable ClassDefTable2 => new(this.Memory[this.ClassDef2Offset..]); + + public (ValueRecord Record1, ValueRecord Record2) this[(int Class1Index, int Class2Index) v] => + this[v.Class1Index, v.Class2Index]; + + public (ValueRecord Record1, ValueRecord Record2) this[int class1Index, int class2Index] + { + get + { + if (class1Index < 0 || class1Index >= this.Class1Count) + throw new IndexOutOfRangeException(); + + if (class2Index < 0 || class2Index >= this.Class2Count) + throw new IndexOutOfRangeException(); + + var offset = 16 + (this.PairSize * ((class1Index * this.Class2Count) + class2Index)); + return ( + new(this.Memory.Slice(offset, this.PairValue1Size), this.ValueFormat1), + new( + this.Memory.Slice(offset + this.PairValue1Size, this.PairValue2Size), + this.ValueFormat2)); + } + } + + public IEnumerable ExtractAdvanceX() + { + if ((this.ValueFormat1 & ValueFormat.AdvanceX) == 0 && + (this.ValueFormat2 & ValueFormat.AdvanceX) == 0) + { + yield break; + } + + var classes1 = this.ClassDefTable1.Enumerate() + .GroupBy(x => x.Class, x => x.GlyphId) + .ToImmutableDictionary(x => x.Key, x => x.ToImmutableSortedSet()); + + var classes2 = this.ClassDefTable2.Enumerate() + .GroupBy(x => x.Class, x => x.GlyphId) + .ToImmutableDictionary(x => x.Key, x => x.ToImmutableSortedSet()); + + foreach (var class1 in Enumerable.Range(0, this.Class1Count)) + { + if (!classes1.TryGetValue((ushort)class1, out var glyphs1)) + continue; + + foreach (var class2 in Enumerable.Range(0, this.Class2Count)) + { + if (!classes2.TryGetValue((ushort)class2, out var glyphs2)) + continue; + + (ValueRecord, ValueRecord) record; + try + { + record = this[class1, class2]; + } + catch (ArgumentOutOfRangeException) + { + yield break; + } + catch (IndexOutOfRangeException) + { + yield break; + } + + var val = record.Item1.AdvanceX + record.Item2.PlacementX; + if (val == 0) + continue; + + foreach (var glyph1 in glyphs1) + { + foreach (var glyph2 in glyphs2) + { + yield return new(glyph1, glyph2, (short)val); + } + } + } + } + } + } + } + + public readonly struct ExtensionPositioningSubtableFormat1 + { + public readonly PointerSpan Memory; + + public ExtensionPositioningSubtableFormat1(PointerSpan memory) => this.Memory = memory; + + public ushort Format => this.Memory.ReadU16Big(0); + + public LookupType ExtensionLookupType => this.Memory.ReadEnumBig(2); + + public int ExtensionOffset => this.Memory.ReadI32Big(4); + + public PointerSpan ExtensionData => this.Memory[this.ExtensionOffset..]; + } + } + + private readonly struct Head + { + // https://docs.microsoft.com/en-us/typography/opentype/spec/head + // https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6head.html + + public const uint MagicNumberValue = 0x5F0F3CF5; + public static readonly TagStruct DirectoryTableTag = new('h', 'e', 'a', 'd'); + + public readonly PointerSpan Memory; + + public Head(SfntFile file) + : this(file[DirectoryTableTag]) + { + } + + public Head(PointerSpan memory) => this.Memory = memory; + + [Flags] + public enum HeadFlags : ushort + { + BaselineForFontAtZeroY = 1 << 0, + LeftSideBearingAtZeroX = 1 << 1, + InstructionsDependOnPointSize = 1 << 2, + ForcePpemsInteger = 1 << 3, + InstructionsAlterAdvanceWidth = 1 << 4, + VerticalLayout = 1 << 5, + Reserved6 = 1 << 6, + RequiresLayoutForCorrectLinguisticRendering = 1 << 7, + IsAatFont = 1 << 8, + ContainsRtlGlyph = 1 << 9, + ContainsIndicStyleRearrangementEffects = 1 << 10, + Lossless = 1 << 11, + ProduceCompatibleMetrics = 1 << 12, + OptimizedForClearType = 1 << 13, + IsLastResortFont = 1 << 14, + Reserved15 = 1 << 15, + } + + [Flags] + public enum MacStyleFlags : ushort + { + Bold = 1 << 0, + Italic = 1 << 1, + Underline = 1 << 2, + Outline = 1 << 3, + Shadow = 1 << 4, + Condensed = 1 << 5, + Extended = 1 << 6, + } + + public Fixed Version => new(this.Memory); + + public Fixed FontRevision => new(this.Memory[4..]); + + public uint ChecksumAdjustment => this.Memory.ReadU32Big(8); + + public uint MagicNumber => this.Memory.ReadU32Big(12); + + public HeadFlags Flags => this.Memory.ReadEnumBig(16); + + public ushort UnitsPerEm => this.Memory.ReadU16Big(18); + + public ulong CreatedTimestamp => this.Memory.ReadU64Big(20); + + public ulong ModifiedTimestamp => this.Memory.ReadU64Big(28); + + public ushort MinX => this.Memory.ReadU16Big(36); + + public ushort MinY => this.Memory.ReadU16Big(38); + + public ushort MaxX => this.Memory.ReadU16Big(40); + + public ushort MaxY => this.Memory.ReadU16Big(42); + + public MacStyleFlags MacStyle => this.Memory.ReadEnumBig(44); + + public ushort LowestRecommendedPpem => this.Memory.ReadU16Big(46); + + public ushort FontDirectionHint => this.Memory.ReadU16Big(48); + + public ushort IndexToLocFormat => this.Memory.ReadU16Big(50); + + public ushort GlyphDataFormat => this.Memory.ReadU16Big(52); + } + + private readonly struct Kern + { + // https://docs.microsoft.com/en-us/typography/opentype/spec/kern + // https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6kern.html + + public static readonly TagStruct DirectoryTableTag = new('k', 'e', 'r', 'n'); + + public readonly PointerSpan Memory; + + public Kern(SfntFile file) + : this(file[DirectoryTableTag]) + { + } + + public Kern(PointerSpan memory) => this.Memory = memory; + + public ushort Version => this.Memory.ReadU16Big(0); + + public IEnumerable EnumerateHorizontalPairs() => this.Version switch + { + 0 => new Version0(this.Memory).EnumerateHorizontalPairs(), + 1 => new Version1(this.Memory).EnumerateHorizontalPairs(), + _ => Array.Empty(), + }; + + public readonly struct Format0 + { + public readonly PointerSpan Memory; + + public Format0(PointerSpan memory) => this.Memory = memory; + + public ushort PairCount => this.Memory.ReadU16Big(0); + + public ushort SearchRange => this.Memory.ReadU16Big(2); + + public ushort EntrySelector => this.Memory.ReadU16Big(4); + + public ushort RangeShift => this.Memory.ReadU16Big(6); + + public BigEndianPointerSpan Pairs => new( + this.Memory[8..].As(this.PairCount), + KerningPair.ReverseEndianness); + } + + public readonly struct Version0 + { + public readonly PointerSpan Memory; + + public Version0(PointerSpan memory) => this.Memory = memory; + + [Flags] + public enum CoverageFlags : byte + { + Horizontal = 1 << 0, + Minimum = 1 << 1, + CrossStream = 1 << 2, + Override = 1 << 3, + } + + public ushort Version => this.Memory.ReadU16Big(0); + + public ushort NumSubtables => this.Memory.ReadU16Big(2); + + public PointerSpan Data => this.Memory[4..]; + + public IEnumerable EnumerateSubtables() + { + var data = this.Data; + for (var i = 0; i < this.NumSubtables && !data.IsEmpty; i++) + { + var st = new Subtable(data); + data = data[st.Length..]; + yield return st; + } + } + + public IEnumerable EnumerateHorizontalPairs() + { + var accumulator = new Dictionary<(ushort Left, ushort Right), short>(); + foreach (var subtable in this.EnumerateSubtables()) + { + var isOverride = (subtable.Flags & CoverageFlags.Override) != 0; + var isMinimum = (subtable.Flags & CoverageFlags.Minimum) != 0; + foreach (var t in subtable.EnumeratePairs()) + { + if (isOverride) + { + accumulator[(t.Left, t.Right)] = t.Value; + } + else if (isMinimum) + { + accumulator[(t.Left, t.Right)] = Math.Max( + accumulator.GetValueOrDefault((t.Left, t.Right), t.Value), + t.Value); + } + else + { + accumulator[(t.Left, t.Right)] = (short)( + accumulator.GetValueOrDefault( + (t.Left, t.Right)) + t.Value); + } + } + } + + return accumulator.Select( + x => new KerningPair { Left = x.Key.Left, Right = x.Key.Right, Value = x.Value }); + } + + public readonly struct Subtable + { + public readonly PointerSpan Memory; + + public Subtable(PointerSpan memory) => this.Memory = memory; + + public ushort Version => this.Memory.ReadU16Big(0); + + public ushort Length => this.Memory.ReadU16Big(2); + + public byte Format => this.Memory[4]; + + public CoverageFlags Flags => this.Memory.ReadEnumBig(5); + + public PointerSpan Data => this.Memory[6..]; + + public IEnumerable EnumeratePairs() => this.Format switch + { + 0 => new Format0(this.Data).Pairs, + _ => Array.Empty(), + }; + } + } + + public readonly struct Version1 + { + public readonly PointerSpan Memory; + + public Version1(PointerSpan memory) => this.Memory = memory; + + [Flags] + public enum CoverageFlags : byte + { + Vertical = 1 << 0, + CrossStream = 1 << 1, + Variation = 1 << 2, + } + + public Fixed Version => new(this.Memory); + + public int NumSubtables => this.Memory.ReadI16Big(4); + + public PointerSpan Data => this.Memory[8..]; + + public IEnumerable EnumerateSubtables() + { + var data = this.Data; + for (var i = 0; i < this.NumSubtables && !data.IsEmpty; i++) + { + var st = new Subtable(data); + data = data[st.Length..]; + yield return st; + } + } + + public IEnumerable EnumerateHorizontalPairs() => this + .EnumerateSubtables() + .Where(x => x.Flags == 0) + .SelectMany(x => x.EnumeratePairs()); + + public readonly struct Subtable + { + public readonly PointerSpan Memory; + + public Subtable(PointerSpan memory) => this.Memory = memory; + + public int Length => this.Memory.ReadI32Big(0); + + public byte Format => this.Memory[4]; + + public CoverageFlags Flags => this.Memory.ReadEnumBig(5); + + public ushort TupleIndex => this.Memory.ReadU16Big(6); + + public PointerSpan Data => this.Memory[8..]; + + public IEnumerable EnumeratePairs() => this.Format switch + { + 0 => new Format0(this.Data).Pairs, + _ => Array.Empty(), + }; + } + } + } + + private readonly struct Name + { + // https://docs.microsoft.com/en-us/typography/opentype/spec/name + // https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6name.html + + public static readonly TagStruct DirectoryTableTag = new('n', 'a', 'm', 'e'); + + public readonly PointerSpan Memory; + + public Name(SfntFile file) + : this(file[DirectoryTableTag]) + { + } + + public Name(PointerSpan memory) => this.Memory = memory; + + public ushort Version => this.Memory.ReadU16Big(0); + + public ushort Count => this.Memory.ReadU16Big(2); + + public ushort StorageOffset => this.Memory.ReadU16Big(4); + + public BigEndianPointerSpan NameRecords => new( + this.Memory[6..].As(this.Count), + NameRecord.ReverseEndianness); + + public ushort LanguageCount => + this.Version == 0 ? (ushort)0 : this.Memory.ReadU16Big(6 + this.NameRecords.ByteCount); + + public BigEndianPointerSpan LanguageRecords => this.Version == 0 + ? default + : new( + this.Memory[ + (8 + this.NameRecords + .ByteCount)..] + .As( + this.LanguageCount), + LanguageRecord.ReverseEndianness); + + public PointerSpan Storage => this.Memory[this.StorageOffset..]; + + public string this[in NameRecord record] => + record.PlatformAndEncoding.Decode(this.Storage.Span.Slice(record.StringOffset, record.Length)); + + public string this[in LanguageRecord record] => + Encoding.ASCII.GetString(this.Storage.Span.Slice(record.LanguageTagOffset, record.Length)); + + public struct NameRecord + { + public PlatformAndEncoding PlatformAndEncoding; + public ushort LanguageId; + public NameId NameId; + public ushort Length; + public ushort StringOffset; + + public NameRecord(PointerSpan span) + { + this.PlatformAndEncoding = new(span); + var offset = Unsafe.SizeOf(); + span.ReadBig(ref offset, out this.LanguageId); + span.ReadBig(ref offset, out this.NameId); + span.ReadBig(ref offset, out this.Length); + span.ReadBig(ref offset, out this.StringOffset); + } + + public static NameRecord ReverseEndianness(NameRecord value) => new() + { + PlatformAndEncoding = PlatformAndEncoding.ReverseEndianness(value.PlatformAndEncoding), + LanguageId = BinaryPrimitives.ReverseEndianness(value.LanguageId), + NameId = (NameId)BinaryPrimitives.ReverseEndianness((ushort)value.NameId), + Length = BinaryPrimitives.ReverseEndianness(value.Length), + StringOffset = BinaryPrimitives.ReverseEndianness(value.StringOffset), + }; + } + + public struct LanguageRecord + { + public ushort Length; + public ushort LanguageTagOffset; + + public LanguageRecord(PointerSpan span) + { + var offset = 0; + span.ReadBig(ref offset, out this.Length); + span.ReadBig(ref offset, out this.LanguageTagOffset); + } + + public static LanguageRecord ReverseEndianness(LanguageRecord value) => new() + { + Length = BinaryPrimitives.ReverseEndianness(value.Length), + LanguageTagOffset = BinaryPrimitives.ReverseEndianness(value.LanguageTagOffset), + }; + } + } +} diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.cs new file mode 100644 index 000000000..1d437d56d --- /dev/null +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.cs @@ -0,0 +1,135 @@ +using System.Buffers.Binary; +using System.Collections.Generic; +using System.Linq; + +using Dalamud.Interface.Utility; + +using ImGuiNET; + +namespace Dalamud.Interface.ManagedFontAtlas.Internals; + +/// +/// Deals with TrueType. +/// +internal static partial class TrueTypeUtils +{ + /// + /// Checks whether the given will fail in , + /// and throws an appropriate exception if it is the case. + /// + /// The font config. + public static unsafe void CheckImGuiCompatibleOrThrow(in ImFontConfig fontConfig) + { + var ranges = fontConfig.GlyphRanges; + var sfnt = AsSfntFile(fontConfig); + var cmap = new Cmap(sfnt); + if (cmap.UnicodeTable is not { } unicodeTable) + throw new NotSupportedException("The font does not have a compatible Unicode character mapping table."); + if (unicodeTable.All(x => !ImGuiHelpers.IsCodepointInSuppliedGlyphRangesUnsafe(x.Key, ranges))) + throw new NotSupportedException("The font does not have any glyph that falls under the requested range."); + } + + /// + /// Enumerates through horizontal pair adjustments of a kern and gpos tables. + /// + /// The font config. + /// The enumerable of pair adjustments. Distance values need to be multiplied by font size in pixels. + public static IEnumerable<(char Left, char Right, float Distance)> ExtractHorizontalPairAdjustments( + ImFontConfig fontConfig) + { + float multiplier; + Dictionary glyphToCodepoints; + Gpos gpos = default; + Kern kern = default; + + try + { + var sfnt = AsSfntFile(fontConfig); + var head = new Head(sfnt); + multiplier = 3f / 4 / head.UnitsPerEm; + + if (new Cmap(sfnt).UnicodeTable is not { } table) + yield break; + + if (sfnt.ContainsKey(Kern.DirectoryTableTag)) + kern = new(sfnt); + else if (sfnt.ContainsKey(Gpos.DirectoryTableTag)) + gpos = new(sfnt); + else + yield break; + + glyphToCodepoints = table + .GroupBy(x => x.Value, x => x.Key) + .OrderBy(x => x.Key) + .ToDictionary( + x => x.Key, + x => x.Where(y => y <= ushort.MaxValue) + .Select(y => (char)y) + .ToArray()); + } + catch + { + // don't care; give up + yield break; + } + + if (kern.Memory.Count != 0) + { + foreach (var pair in kern.EnumerateHorizontalPairs()) + { + if (!glyphToCodepoints.TryGetValue(pair.Left, out var leftChars)) + continue; + if (!glyphToCodepoints.TryGetValue(pair.Right, out var rightChars)) + continue; + + foreach (var l in leftChars) + { + foreach (var r in rightChars) + yield return (l, r, pair.Value * multiplier); + } + } + } + else if (gpos.Memory.Count != 0) + { + foreach (var pair in gpos.ExtractAdvanceX()) + { + if (!glyphToCodepoints.TryGetValue(pair.Left, out var leftChars)) + continue; + if (!glyphToCodepoints.TryGetValue(pair.Right, out var rightChars)) + continue; + + foreach (var l in leftChars) + { + foreach (var r in rightChars) + yield return (l, r, pair.Value * multiplier); + } + } + } + } + + private static unsafe SfntFile AsSfntFile(in ImFontConfig fontConfig) + { + var memory = new PointerSpan((byte*)fontConfig.FontData, fontConfig.FontDataSize); + if (memory.Length < 4) + throw new NotSupportedException("File is too short to even have a magic."); + + var magic = memory.ReadU32Big(0); + if (BitConverter.IsLittleEndian) + magic = BinaryPrimitives.ReverseEndianness(magic); + + if (magic == SfntFile.FileTagTrueType1.NativeValue) + return new(memory); + if (magic == SfntFile.FileTagType1.NativeValue) + return new(memory); + if (magic == SfntFile.FileTagOpenTypeWithCff.NativeValue) + return new(memory); + if (magic == SfntFile.FileTagOpenType1_0.NativeValue) + return new(memory); + if (magic == SfntFile.FileTagTrueTypeApple.NativeValue) + return new(memory); + if (magic == TtcFile.FileTag.NativeValue) + return new TtcFile(memory)[fontConfig.FontNo]; + + throw new NotSupportedException($"The given file with the magic 0x{magic:X08} is not supported."); + } +} diff --git a/Dalamud/Interface/ManagedFontAtlas/SafeFontConfig.cs b/Dalamud/Interface/ManagedFontAtlas/SafeFontConfig.cs new file mode 100644 index 000000000..cb7f7c65a --- /dev/null +++ b/Dalamud/Interface/ManagedFontAtlas/SafeFontConfig.cs @@ -0,0 +1,306 @@ +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Text; + +using ImGuiNET; + +namespace Dalamud.Interface.ManagedFontAtlas; + +/// +/// Managed version of , to avoid unnecessary heap allocation and use of unsafe blocks. +/// +public struct SafeFontConfig +{ + /// + /// The raw config. + /// + public ImFontConfig Raw; + + /// + /// Initializes a new instance of the struct. + /// + public SafeFontConfig() + { + this.OversampleH = 1; + this.OversampleV = 1; + this.PixelSnapH = true; + this.GlyphMaxAdvanceX = float.MaxValue; + this.RasterizerMultiply = 1f; + this.RasterizerGamma = 1.4f; + this.EllipsisChar = unchecked((char)-1); + this.Raw.FontDataOwnedByAtlas = 1; + } + + /// + /// Initializes a new instance of the struct, + /// copying applicable values from an existing instance of . + /// + /// Config to copy from. + public unsafe SafeFontConfig(ImFontConfigPtr config) + : this() + { + if (config.NativePtr is not null) + { + this.Raw = *config.NativePtr; + this.Raw.GlyphRanges = null; + } + } + + /// + /// Gets or sets the index of font within a TTF/OTF file. + /// + public int FontNo + { + get => this.Raw.FontNo; + set => this.Raw.FontNo = EnsureRange(value, 0, int.MaxValue); + } + + /// + /// Gets or sets the desired size of the new font, in pixels.
+ /// Effectively, this is the line height.
+ /// Value is tied with . + ///
+ public float SizePx + { + get => this.Raw.SizePixels; + set => this.Raw.SizePixels = EnsureRange(value, float.Epsilon, float.MaxValue); + } + + /// + /// Gets or sets the desired size of the new font, in points.
+ /// Effectively, this is the line height.
+ /// Value is tied with . + ///
+ public float SizePt + { + get => (this.Raw.SizePixels * 3) / 4; + set => this.Raw.SizePixels = EnsureRange((value * 4) / 3, float.Epsilon, float.MaxValue); + } + + /// + /// Gets or sets the horizontal oversampling pixel count.
+ /// Rasterize at higher quality for sub-pixel positioning.
+ /// Note the difference between 2 and 3 is minimal so you can reduce this to 2 to save memory.
+ /// Read https://github.com/nothings/stb/blob/master/tests/oversample/README.md for details. + ///
+ public int OversampleH + { + get => this.Raw.OversampleH; + set => this.Raw.OversampleH = EnsureRange(value, 1, int.MaxValue); + } + + /// + /// Gets or sets the vertical oversampling pixel count.
+ /// Rasterize at higher quality for sub-pixel positioning.
+ /// This is not really useful as we don't use sub-pixel positions on the Y axis. + ///
+ public int OversampleV + { + get => this.Raw.OversampleV; + set => this.Raw.OversampleV = EnsureRange(value, 1, int.MaxValue); + } + + /// + /// Gets or sets a value indicating whether to align every glyph to pixel boundary.
+ /// Useful e.g. if you are merging a non-pixel aligned font with the default font.
+ /// If enabled, you can set and to 1. + ///
+ public bool PixelSnapH + { + get => this.Raw.PixelSnapH != 0; + set => this.Raw.PixelSnapH = value ? (byte)1 : (byte)0; + } + + /// + /// Gets or sets the extra spacing (in pixels) between glyphs.
+ /// Only X axis is supported for now.
+ /// Effectively, it is the letter spacing. + ///
+ public Vector2 GlyphExtraSpacing + { + get => this.Raw.GlyphExtraSpacing; + set => this.Raw.GlyphExtraSpacing = new( + EnsureRange(value.X, float.MinValue, float.MaxValue), + EnsureRange(value.Y, float.MinValue, float.MaxValue)); + } + + /// + /// Gets or sets the offset all glyphs from this font input.
+ /// Use this to offset fonts vertically when merging multiple fonts. + ///
+ public Vector2 GlyphOffset + { + get => this.Raw.GlyphOffset; + set => this.Raw.GlyphOffset = new( + EnsureRange(value.X, float.MinValue, float.MaxValue), + EnsureRange(value.Y, float.MinValue, float.MaxValue)); + } + + /// + /// Gets or sets the glyph ranges, which is a user-provided list of Unicode range. + /// Each range has 2 values, and values are inclusive.
+ /// The list must be zero-terminated.
+ /// If empty or null, then all the glyphs from the font that is in the range of UCS-2 will be added. + ///
+ public ushort[]? GlyphRanges { get; set; } + + /// + /// Gets or sets the minimum AdvanceX for glyphs.
+ /// Set only to align font icons.
+ /// Set both / to enforce mono-space font. + ///
+ public float GlyphMinAdvanceX + { + get => this.Raw.GlyphMinAdvanceX; + set => this.Raw.GlyphMinAdvanceX = + float.IsFinite(value) + ? value + : throw new ArgumentOutOfRangeException( + nameof(value), + value, + $"{nameof(this.GlyphMinAdvanceX)} must be a finite number."); + } + + /// + /// Gets or sets the maximum AdvanceX for glyphs. + /// + public float GlyphMaxAdvanceX + { + get => this.Raw.GlyphMaxAdvanceX; + set => this.Raw.GlyphMaxAdvanceX = + float.IsFinite(value) + ? value + : throw new ArgumentOutOfRangeException( + nameof(value), + value, + $"{nameof(this.GlyphMaxAdvanceX)} must be a finite number."); + } + + /// + /// Gets or sets a value that either brightens (>1.0f) or darkens (<1.0f) the font output.
+ /// Brightening small fonts may be a good workaround to make them more readable. + ///
+ public float RasterizerMultiply + { + get => this.Raw.RasterizerMultiply; + set => this.Raw.RasterizerMultiply = EnsureRange(value, float.Epsilon, float.MaxValue); + } + + /// + /// Gets or sets the gamma value for fonts. + /// + public float RasterizerGamma + { + get => this.Raw.RasterizerGamma; + set => this.Raw.RasterizerGamma = EnsureRange(value, float.Epsilon, float.MaxValue); + } + + /// + /// Gets or sets a value explicitly specifying unicode codepoint of the ellipsis character.
+ /// When fonts are being merged first specified ellipsis will be used. + ///
+ public char EllipsisChar + { + get => (char)this.Raw.EllipsisChar; + set => this.Raw.EllipsisChar = value; + } + + /// + /// Gets or sets the desired name of the new font. Names longer than 40 bytes will be partially lost. + /// + public unsafe string Name + { + get + { + fixed (void* pName = this.Raw.Name) + { + var span = new ReadOnlySpan(pName, 40); + var firstNull = span.IndexOf((byte)0); + if (firstNull != -1) + span = span[..firstNull]; + return Encoding.UTF8.GetString(span); + } + } + + set + { + fixed (void* pName = this.Raw.Name) + { + var span = new Span(pName, 40); + Encoding.UTF8.GetBytes(value, span); + } + } + } + + /// + /// Gets or sets the desired font to merge with, if set. + /// + public unsafe ImFontPtr MergeFont + { + get => this.Raw.DstFont is not null ? this.Raw.DstFont : default; + set + { + this.Raw.MergeMode = value.NativePtr is null ? (byte)0 : (byte)1; + this.Raw.DstFont = value.NativePtr is null ? default : value.NativePtr; + } + } + + /// + /// Throws with appropriate messages, + /// if this has invalid values. + /// + public readonly void ThrowOnInvalidValues() + { + if (!(this.Raw.FontNo >= 0)) + throw new ArgumentException($"{nameof(this.FontNo)} must not be a negative number."); + + if (!(this.Raw.SizePixels > 0)) + throw new ArgumentException($"{nameof(this.SizePx)} must be a positive number."); + + if (!(this.Raw.OversampleH >= 1)) + throw new ArgumentException($"{nameof(this.OversampleH)} must be a negative number."); + + if (!(this.Raw.OversampleV >= 1)) + throw new ArgumentException($"{nameof(this.OversampleV)} must be a negative number."); + + if (!float.IsFinite(this.Raw.GlyphMinAdvanceX)) + throw new ArgumentException($"{nameof(this.GlyphMinAdvanceX)} must be a finite number."); + + if (!float.IsFinite(this.Raw.GlyphMaxAdvanceX)) + throw new ArgumentException($"{nameof(this.GlyphMaxAdvanceX)} must be a finite number."); + + if (!(this.Raw.RasterizerMultiply > 0)) + throw new ArgumentException($"{nameof(this.RasterizerMultiply)} must be a positive number."); + + if (!(this.Raw.RasterizerGamma > 0)) + throw new ArgumentException($"{nameof(this.RasterizerGamma)} must be a positive number."); + + if (this.GlyphRanges is { Length: > 0 } ranges) + { + if (ranges[0] == 0) + { + throw new ArgumentException( + "Font ranges cannot start with 0.", + nameof(this.GlyphRanges)); + } + + if (ranges[(ranges.Length - 1) & ~1] != 0) + { + throw new ArgumentException( + "Font ranges must terminate with a zero at even indices.", + nameof(this.GlyphRanges)); + } + } + } + + private static T EnsureRange(T value, T min, T max, [CallerMemberName] string callerName = "") + where T : INumber + { + if (value < min) + throw new ArgumentOutOfRangeException(callerName, value, $"{callerName} cannot be less than {min}."); + if (value > max) + throw new ArgumentOutOfRangeException(callerName, value, $"{callerName} cannot be more than {max}."); + + return value; + } +} diff --git a/Dalamud/Interface/UiBuilder.cs b/Dalamud/Interface/UiBuilder.cs index dd2e5bad3..a477ec09e 100644 --- a/Dalamud/Interface/UiBuilder.cs +++ b/Dalamud/Interface/UiBuilder.cs @@ -1,4 +1,3 @@ -using System; using System.Collections.Generic; using System.Diagnostics; using System.Threading.Tasks; @@ -12,6 +11,8 @@ using Dalamud.Interface.GameFonts; using Dalamud.Interface.Internal; using Dalamud.Interface.Internal.ManagedAsserts; using Dalamud.Interface.Internal.Notifications; +using Dalamud.Interface.ManagedFontAtlas; +using Dalamud.Interface.ManagedFontAtlas.Internals; using Dalamud.Utility; using ImGuiNET; using ImGuiScene; @@ -30,11 +31,13 @@ public sealed class UiBuilder : IDisposable private readonly HitchDetector hitchDetector; private readonly string namespaceName; private readonly InterfaceManager interfaceManager = Service.Get(); - private readonly GameFontManager gameFontManager = Service.Get(); + private readonly Framework framework = Service.Get(); [ServiceManager.ServiceDependency] private readonly DalamudConfiguration configuration = Service.Get(); + private readonly DisposeSafety.ScopedFinalizer scopedFinalizer = new(); + private bool hasErrorWindow = false; private bool lastFrameUiHideState = false; @@ -45,14 +48,32 @@ public sealed class UiBuilder : IDisposable /// The plugin namespace. internal UiBuilder(string namespaceName) { - this.stopwatch = new Stopwatch(); - this.hitchDetector = new HitchDetector($"UiBuilder({namespaceName})", this.configuration.UiBuilderHitch); - this.namespaceName = namespaceName; + try + { + this.stopwatch = new Stopwatch(); + this.hitchDetector = new HitchDetector($"UiBuilder({namespaceName})", this.configuration.UiBuilderHitch); + this.namespaceName = namespaceName; - this.interfaceManager.Draw += this.OnDraw; - this.interfaceManager.BuildFonts += this.OnBuildFonts; - this.interfaceManager.AfterBuildFonts += this.OnAfterBuildFonts; - this.interfaceManager.ResizeBuffers += this.OnResizeBuffers; + this.interfaceManager.Draw += this.OnDraw; + this.scopedFinalizer.Add(() => this.interfaceManager.Draw -= this.OnDraw); + + this.interfaceManager.ResizeBuffers += this.OnResizeBuffers; + this.scopedFinalizer.Add(() => this.interfaceManager.ResizeBuffers -= this.OnResizeBuffers); + + this.FontAtlas = + this.scopedFinalizer + .Add( + Service + .Get() + .CreateFontAtlas(namespaceName, FontAtlasAutoRebuildMode.Disable)); + this.FontAtlas.BuildStepChange += this.PrivateAtlasOnBuildStepChange; + this.FontAtlas.RebuildRecommend += this.RebuildFonts; + } + catch + { + this.scopedFinalizer.Dispose(); + throw; + } } /// @@ -80,19 +101,19 @@ public sealed class UiBuilder : IDisposable /// Gets or sets an action that is called any time ImGui fonts need to be rebuilt.
/// Any ImFontPtr objects that you store can be invalidated when fonts are rebuilt /// (at any time), so you should both reload your custom fonts and restore those - /// pointers inside this handler.
- /// PLEASE remove this handler inside Dispose, or when you no longer need your fonts! + /// pointers inside this handler. ///
- public event Action BuildFonts; + [Obsolete($"Use {nameof(this.FontAtlas)} instead.", false)] + public event Action? BuildFonts; /// /// Gets or sets an action that is called any time right after ImGui fonts are rebuilt.
/// Any ImFontPtr objects that you store can be invalidated when fonts are rebuilt /// (at any time), so you should both reload your custom fonts and restore those - /// pointers inside this handler.
- /// PLEASE remove this handler inside Dispose, or when you no longer need your fonts! + /// pointers inside this handler. ///
- public event Action AfterBuildFonts; + [Obsolete($"Use {nameof(this.FontAtlas)} instead.", false)] + public event Action? AfterBuildFonts; /// /// Gets or sets an action that is called when plugin UI or interface modifications are supposed to be shown. @@ -107,18 +128,57 @@ public sealed class UiBuilder : IDisposable public event Action HideUi; /// - /// Gets the default Dalamud font based on Noto Sans CJK Medium in 17pt - supporting all game languages and icons. + /// Gets the default Dalamud font size in points. /// + public static float DefaultFontSizePt => InterfaceManager.DefaultFontSizePt; + + /// + /// Gets the default Dalamud font size in pixels. + /// + public static float DefaultFontSizePx => InterfaceManager.DefaultFontSizePx; + + /// + /// Gets the default Dalamud font - supporting all game languages and icons.
+ /// Accessing this static property outside of is dangerous and not supported. + ///
+ /// + /// A font handle corresponding to this font can be obtained with: + /// + /// fontAtlas.NewDelegateFontHandle( + /// e => e.OnPreBuild( + /// tk => tk.AddDalamudDefaultFont(UiBuilder.DefaultFontSizePt))); + /// + /// public static ImFontPtr DefaultFont => InterfaceManager.DefaultFont; /// - /// Gets the default Dalamud icon font based on FontAwesome 5 Free solid in 17pt. + /// Gets the default Dalamud icon font based on FontAwesome 5 Free solid.
+ /// Accessing this static property outside of is dangerous and not supported. ///
+ /// + /// A font handle corresponding to this font can be obtained with: + /// + /// fontAtlas.NewDelegateFontHandle( + /// e => e.OnPreBuild( + /// tk => tk.AddFontAwesomeIconFont(new() { SizePt = UiBuilder.DefaultFontSizePt }))); + /// + /// public static ImFontPtr IconFont => InterfaceManager.IconFont; /// - /// Gets the default Dalamud monospaced font based on Inconsolata Regular in 16pt. + /// Gets the default Dalamud monospaced font based on Inconsolata Regular.
+ /// Accessing this static property outside of is dangerous and not supported. ///
+ /// + /// A font handle corresponding to this font can be obtained with: + /// + /// fontAtlas.NewDelegateFontHandle( + /// e => e.OnPreBuild( + /// tk => tk.AddDalamudAssetFont( + /// DalamudAsset.InconsolataRegular, + /// new() { SizePt = UiBuilder.DefaultFontSizePt }))); + /// + /// public static ImFontPtr MonoFont => InterfaceManager.MonoFont; /// @@ -190,6 +250,11 @@ public sealed class UiBuilder : IDisposable /// public bool UiPrepared => Service.GetNullable() != null; + /// + /// Gets the plugin-private font atlas. + /// + public IFontAtlas FontAtlas { get; } + /// /// Gets or sets a value indicating whether statistics about UI draw time should be collected. /// @@ -319,7 +384,7 @@ public sealed class UiBuilder : IDisposable if (runInFrameworkThread) { return this.InterfaceManagerWithSceneAsync - .ContinueWith(_ => Service.Get().RunOnFrameworkThread(func)) + .ContinueWith(_ => this.framework.RunOnFrameworkThread(func)) .Unwrap(); } else @@ -341,7 +406,7 @@ public sealed class UiBuilder : IDisposable if (runInFrameworkThread) { return this.InterfaceManagerWithSceneAsync - .ContinueWith(_ => Service.Get().RunOnFrameworkThread(func)) + .ContinueWith(_ => this.framework.RunOnFrameworkThread(func)) .Unwrap(); } else @@ -357,19 +422,49 @@ public sealed class UiBuilder : IDisposable ///
/// Font to get. /// Handle to the game font which may or may not be available for use yet. - public GameFontHandle GetGameFontHandle(GameFontStyle style) => this.gameFontManager.NewFontRef(style); + [Obsolete($"Use {nameof(this.FontAtlas)}.{nameof(IFontAtlas.NewGameFontHandle)} instead.", false)] + public GameFontHandle GetGameFontHandle(GameFontStyle style) => new( + (IFontHandle.IInternal)this.FontAtlas.NewGameFontHandle(style), + Service.Get()); /// /// Call this to queue a rebuild of the font atlas.
- /// This will invoke any handlers and ensure that any loaded fonts are - /// ready to be used on the next UI frame. + /// This will invoke any and handlers and ensure that any + /// loaded fonts are ready to be used on the next UI frame. ///
public void RebuildFonts() { Log.Verbose("[FONT] {0} plugin is initiating FONT REBUILD", this.namespaceName); - this.interfaceManager.RebuildFonts(); + if (this.AfterBuildFonts is null && this.BuildFonts is null) + this.FontAtlas.BuildFontsAsync(); + else + this.FontAtlas.BuildFontsOnNextFrame(); } + /// + /// Creates an isolated . + /// + /// Specify when and how to rebuild this atlas. + /// Whether the fonts in the atlas is global scaled. + /// Name for debugging purposes. + /// A new instance of . + /// + /// Use this to create extra font atlases, if you want to create and dispose fonts without having to rebuild all + /// other fonts together.
+ /// If is not , + /// the font rebuilding functions must be called manually. + ///
+ public IFontAtlas CreateFontAtlas( + FontAtlasAutoRebuildMode autoRebuildMode, + bool isGlobalScaled = true, + string? debugName = null) => + this.scopedFinalizer.Add(Service + .Get() + .CreateFontAtlas( + this.namespaceName + ":" + (debugName ?? "custom"), + autoRebuildMode, + isGlobalScaled)); + /// /// Add a notification to the notification queue. /// @@ -392,12 +487,7 @@ public sealed class UiBuilder : IDisposable /// /// Unregister the UiBuilder. Do not call this in plugin code. /// - void IDisposable.Dispose() - { - this.interfaceManager.Draw -= this.OnDraw; - this.interfaceManager.BuildFonts -= this.OnBuildFonts; - this.interfaceManager.ResizeBuffers -= this.OnResizeBuffers; - } + void IDisposable.Dispose() => this.scopedFinalizer.Dispose(); /// /// Open the registered configuration UI, if it exists. @@ -463,8 +553,12 @@ public sealed class UiBuilder : IDisposable this.ShowUi?.InvokeSafely(); } - if (!this.interfaceManager.FontsReady) + // just in case, if something goes wrong, prevent drawing; otherwise it probably will crash. + if (!this.FontAtlas.BuildTask.IsCompletedSuccessfully + && (this.BuildFonts is not null || this.AfterBuildFonts is not null)) + { return; + } ImGui.PushID(this.namespaceName); if (DoStats) @@ -526,14 +620,28 @@ public sealed class UiBuilder : IDisposable this.hitchDetector.Stop(); } - private void OnBuildFonts() + private unsafe void PrivateAtlasOnBuildStepChange(IFontAtlasBuildToolkit e) { - this.BuildFonts?.InvokeSafely(); - } + if (e.IsAsyncBuildOperation) + return; - private void OnAfterBuildFonts() - { - this.AfterBuildFonts?.InvokeSafely(); + e.OnPreBuild( + _ => + { + var prev = ImGui.GetIO().NativePtr->Fonts; + ImGui.GetIO().NativePtr->Fonts = e.NewImAtlas.NativePtr; + this.BuildFonts?.InvokeSafely(); + ImGui.GetIO().NativePtr->Fonts = prev; + }); + + e.OnPostBuild( + _ => + { + var prev = ImGui.GetIO().NativePtr->Fonts; + ImGui.GetIO().NativePtr->Fonts = e.NewImAtlas.NativePtr; + this.AfterBuildFonts?.InvokeSafely(); + ImGui.GetIO().NativePtr->Fonts = prev; + }); } private void OnResizeBuffers() diff --git a/Dalamud/Interface/Utility/ImGuiHelpers.cs b/Dalamud/Interface/Utility/ImGuiHelpers.cs index ad151ec4e..444463d41 100644 --- a/Dalamud/Interface/Utility/ImGuiHelpers.cs +++ b/Dalamud/Interface/Utility/ImGuiHelpers.cs @@ -1,10 +1,15 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.Linq; using System.Numerics; +using System.Reactive.Disposables; using System.Runtime.InteropServices; +using System.Text.Unicode; using Dalamud.Configuration.Internal; using Dalamud.Game.ClientState.Keys; +using Dalamud.Interface.ManagedFontAtlas; +using Dalamud.Interface.ManagedFontAtlas.Internals; using Dalamud.Interface.Utility.Raii; using ImGuiNET; using ImGuiScene; @@ -31,8 +36,7 @@ public static class ImGuiHelpers /// This does not necessarily mean you can call drawing functions. /// public static unsafe bool IsImGuiInitialized => - ImGui.GetCurrentContext() is not (nint)0 // KW: IDEs get mad without the cast, despite being unnecessary - && ImGui.GetIO().NativePtr is not null; + ImGui.GetCurrentContext() != nint.Zero && ImGui.GetIO().NativePtr is not null; /// /// Gets the global Dalamud scale; even available before drawing is ready.
@@ -198,7 +202,7 @@ public static class ImGuiHelpers /// If a positive number is given, numbers will be rounded to this. public static unsafe void AdjustGlyphMetrics(this ImFontPtr fontPtr, float scale, float round = 0f) { - Func rounder = round > 0 ? x => MathF.Round(x * round) / round : x => x; + Func rounder = round > 0 ? x => MathF.Round(x / round) * round : x => x; var font = fontPtr.NativePtr; font->FontSize = rounder(font->FontSize * scale); @@ -310,6 +314,7 @@ public static class ImGuiHelpers glyph->U1, glyph->V1, glyph->AdvanceX * scale); + target.Mark4KPageUsedAfterGlyphAdd((ushort)glyph->Codepoint); changed = true; } else if (!missingOnly) @@ -343,25 +348,18 @@ public static class ImGuiHelpers } if (changed && rebuildLookupTable) - target.BuildLookupTableNonstandard(); - } + { + // ImGui resolves ' ' with FindGlyph, which uses FallbackGlyph. + // FallbackGlyph is resolved after resolving ' '. + // On the first call of BuildLookupTable, called from BuildFonts, FallbackGlyph is set to null, + // making FindGlyph return nullptr. + // On our secondary calls of BuildLookupTable, FallbackGlyph is set to some value that is not null, + // making ImGui attempt to treat whatever was there as a ' '. + // This may cause random glyphs to be sized randomly, if not an access violation exception. + target.NativePtr->FallbackGlyph = null; - /// - /// Call ImFont::BuildLookupTable, after attempting to fulfill some preconditions. - /// - /// The font. - public static unsafe void BuildLookupTableNonstandard(this ImFontPtr font) - { - // ImGui resolves ' ' with FindGlyph, which uses FallbackGlyph. - // FallbackGlyph is resolved after resolving ' '. - // On the first call of BuildLookupTable, called from BuildFonts, FallbackGlyph is set to null, - // making FindGlyph return nullptr. - // On our secondary calls of BuildLookupTable, FallbackGlyph is set to some value that is not null, - // making ImGui attempt to treat whatever was there as a ' '. - // This may cause random glyphs to be sized randomly, if not an access violation exception. - font.NativePtr->FallbackGlyph = null; - - font.BuildLookupTable(); + target.BuildLookupTable(); + } } /// @@ -407,6 +405,103 @@ public static class ImGuiHelpers public static void CenterCursorFor(float itemWidth) => ImGui.SetCursorPosX((int)((ImGui.GetWindowWidth() - itemWidth) / 2)); + /// + /// Allocates memory on the heap using
+ /// Memory must be freed using . + ///
+ /// Note that null is a valid return value when is 0. + ///
+ /// The length of allocated memory. + /// The allocated memory. + /// If returns null. + public static unsafe void* AllocateMemory(int length) + { + // TODO: igMemAlloc takes size_t, which is nint; ImGui.NET apparently interpreted that as uint. + // fix that in ImGui.NET. + switch (length) + { + case 0: + return null; + case < 0: + throw new ArgumentOutOfRangeException( + nameof(length), + length, + $"{nameof(length)} cannot be a negative number."); + default: + var memory = ImGuiNative.igMemAlloc((uint)length); + if (memory is null) + { + throw new OutOfMemoryException( + $"Failed to allocate {length} bytes using {nameof(ImGuiNative.igMemAlloc)}"); + } + + return memory; + } + } + + /// + /// Creates a new instance of with a natively backed memory. + /// + /// The created instance. + /// Disposable you can call. + public static unsafe IDisposable NewFontGlyphRangeBuilderPtrScoped(out ImFontGlyphRangesBuilderPtr builder) + { + builder = new(ImGuiNative.ImFontGlyphRangesBuilder_ImFontGlyphRangesBuilder()); + var ptr = builder.NativePtr; + return Disposable.Create(() => + { + if (ptr != null) + ImGuiNative.ImFontGlyphRangesBuilder_destroy(ptr); + ptr = null; + }); + } + + /// + /// Builds ImGui Glyph Ranges for use with . + /// + /// The builder. + /// Add fallback codepoints to the range. + /// Add ellipsis codepoints to the range. + /// When disposed, the resource allocated for the range will be freed. + public static unsafe ushort[] BuildRangesToArray( + this ImFontGlyphRangesBuilderPtr builder, + bool addFallbackCodepoints = true, + bool addEllipsisCodepoints = true) + { + if (addFallbackCodepoints) + builder.AddText(FontAtlasFactory.FallbackCodepoints); + if (addEllipsisCodepoints) + { + builder.AddText(FontAtlasFactory.EllipsisCodepoints); + builder.AddChar('.'); + } + + builder.BuildRanges(out var vec); + return new ReadOnlySpan((void*)vec.Data, vec.Size).ToArray(); + } + + /// + public static ushort[] CreateImGuiRangesFrom(params UnicodeRange[] ranges) + => CreateImGuiRangesFrom((IEnumerable)ranges); + + /// + /// Creates glyph ranges from .
+ /// Use values from . + ///
+ /// The unicode ranges. + /// The range array that can be used for . + public static ushort[] CreateImGuiRangesFrom(IEnumerable ranges) => + ranges + .Where(x => x.FirstCodePoint <= ushort.MaxValue) + .SelectMany( + x => new[] + { + (ushort)Math.Min(x.FirstCodePoint, ushort.MaxValue), + (ushort)Math.Min(x.FirstCodePoint + x.Length, ushort.MaxValue), + }) + .Append((ushort)0) + .ToArray(); + /// /// Determines whether is empty. /// @@ -415,7 +510,7 @@ public static class ImGuiHelpers public static unsafe bool IsNull(this ImFontPtr ptr) => ptr.NativePtr == null; /// - /// Determines whether is not null and loaded. + /// Determines whether is empty. /// /// The pointer. /// Whether it is empty. @@ -427,6 +522,27 @@ public static class ImGuiHelpers /// The pointer. /// Whether it is empty. public static unsafe bool IsNull(this ImFontAtlasPtr ptr) => ptr.NativePtr == null; + + /// + /// If is default, then returns . + /// + /// The self. + /// The other. + /// if it is not default; otherwise, . + public static unsafe ImFontPtr OrElse(this ImFontPtr self, ImFontPtr other) => + self.NativePtr is null ? other : self; + + /// + /// Mark 4K page as used, after adding a codepoint to a font. + /// + /// The font. + /// The codepoint. + internal static unsafe void Mark4KPageUsedAfterGlyphAdd(this ImFontPtr font, ushort codepoint) + { + // Mark 4K page as used + var pageIndex = unchecked((ushort)(codepoint / 4096)); + font.NativePtr->Used4kPagesMap[pageIndex >> 3] |= unchecked((byte)(1 << (pageIndex & 7))); + } /// /// Finds the corresponding ImGui viewport ID for the given window handle. @@ -448,6 +564,89 @@ public static class ImGuiHelpers return -1; } + /// + /// Attempts to validate that is valid. + /// + /// The font pointer. + /// The exception, if any occurred during validation. + internal static unsafe Exception? ValidateUnsafe(this ImFontPtr fontPtr) + { + try + { + var font = fontPtr.NativePtr; + if (font is null) + throw new NullReferenceException("The font is null."); + + _ = Marshal.ReadIntPtr((nint)font); + if (font->IndexedHotData.Data != 0) + _ = Marshal.ReadIntPtr(font->IndexedHotData.Data); + if (font->FrequentKerningPairs.Data != 0) + _ = Marshal.ReadIntPtr(font->FrequentKerningPairs.Data); + if (font->IndexLookup.Data != 0) + _ = Marshal.ReadIntPtr(font->IndexLookup.Data); + if (font->Glyphs.Data != 0) + _ = Marshal.ReadIntPtr(font->Glyphs.Data); + if (font->KerningPairs.Data != 0) + _ = Marshal.ReadIntPtr(font->KerningPairs.Data); + if (font->ConfigDataCount == 0 && font->ConfigData is not null) + throw new InvalidOperationException("ConfigDataCount == 0 but ConfigData is not null?"); + if (font->ConfigDataCount != 0 && font->ConfigData is null) + throw new InvalidOperationException("ConfigDataCount != 0 but ConfigData is null?"); + if (font->ConfigData is not null) + _ = Marshal.ReadIntPtr((nint)font->ConfigData); + if (font->FallbackGlyph is not null + && ((nint)font->FallbackGlyph < font->Glyphs.Data || (nint)font->FallbackGlyph >= font->Glyphs.Data)) + throw new InvalidOperationException("FallbackGlyph is not in range of Glyphs.Data"); + if (font->FallbackHotData is not null + && ((nint)font->FallbackHotData < font->IndexedHotData.Data + || (nint)font->FallbackHotData >= font->IndexedHotData.Data)) + throw new InvalidOperationException("FallbackGlyph is not in range of Glyphs.Data"); + if (font->ContainerAtlas is not null) + _ = Marshal.ReadIntPtr((nint)font->ContainerAtlas); + } + catch (Exception e) + { + return e; + } + + return null; + } + + /// + /// Updates the fallback char of . + /// + /// The font. + /// The fallback character. + internal static unsafe void UpdateFallbackChar(this ImFontPtr font, char c) + { + font.FallbackChar = c; + font.NativePtr->FallbackHotData = + (ImFontGlyphHotData*)((ImFontGlyphHotDataReal*)font.IndexedHotData.Data + font.FallbackChar); + } + + /// + /// Determines if the supplied codepoint is inside the given range, + /// in format of . + /// + /// The codepoint. + /// The ranges. + /// Whether it is the case. + internal static unsafe bool IsCodepointInSuppliedGlyphRangesUnsafe(int codepoint, ushort* rangePtr) + { + if (codepoint is <= 0 or >= ushort.MaxValue) + return false; + + while (*rangePtr != 0) + { + var from = *rangePtr++; + var to = *rangePtr++; + if (from <= codepoint && codepoint <= to) + return true; + } + + return false; + } + /// /// Get data needed for each new frame. ///