From b5696afe94b9ace8c58323a751b5bb88cae9cece Mon Sep 17 00:00:00 2001 From: goat <16760685+goaaats@users.noreply.github.com> Date: Thu, 18 Jan 2024 21:37:05 +0100 Subject: [PATCH] Revert "IFontAtlas: font atlas per plugin" --- .../Internal/DalamudConfiguration.cs | 7 +- Dalamud/Interface/GameFonts/FdtFileView.cs | 159 -- .../GameFonts/GameFontFamilyAndSize.cs | 25 +- .../GameFontFamilyAndSizeAttribute.cs | 37 - Dalamud/Interface/GameFonts/GameFontHandle.cs | 85 +- .../Interface/GameFonts/GameFontManager.cs | 507 ++++++ Dalamud/Interface/GameFonts/GameFontStyle.cs | 37 +- Dalamud/Interface/Internal/DalamudIme.cs | 7 +- .../Interface/Internal/DalamudInterface.cs | 13 +- .../Interface/Internal/InterfaceManager.cs | 960 +++++++++--- .../Internal/Windows/ChangelogWindow.cs | 62 +- .../Internal/Windows/Data/DataWindow.cs | 8 +- .../Widgets/GamePrebakedFontsTestWidget.cs | 213 --- .../Windows/Settings/SettingsWindow.cs | 29 +- .../Windows/Settings/Tabs/SettingsTabAbout.cs | 30 +- .../Windows/Settings/Tabs/SettingsTabLook.cs | 50 +- .../Internal/Windows/TitleScreenMenuWindow.cs | 63 +- .../FontAtlasAutoRebuildMode.cs | 22 - .../ManagedFontAtlas/FontAtlasBuildStep.cs | 38 - .../FontAtlasBuildStepDelegate.cs | 15 - .../FontAtlasBuildToolkitUtilities.cs | 133 -- .../Interface/ManagedFontAtlas/IFontAtlas.cs | 141 -- .../IFontAtlasBuildToolkit.cs | 67 - .../IFontAtlasBuildToolkitPostBuild.cs | 26 - .../IFontAtlasBuildToolkitPostPromotion.cs | 33 - .../IFontAtlasBuildToolkitPreBuild.cs | 186 --- .../Interface/ManagedFontAtlas/IFontHandle.cs | 42 - .../Internals/DelegateFontHandle.cs | 334 ---- .../FontAtlasFactory.BuildToolkit.cs | 682 -------- .../FontAtlasFactory.Implementation.cs | 726 --------- .../Internals/FontAtlasFactory.cs | 368 ----- .../Internals/GamePrebakedFontHandle.cs | 857 ---------- .../Internals/IFontHandleManager.cs | 32 - .../Internals/IFontHandleSubstance.cs | 54 - .../Internals/TrueType.Common.cs | 203 --- .../Internals/TrueType.Enums.cs | 84 - .../Internals/TrueType.Files.cs | 148 -- .../Internals/TrueType.GposGsub.cs | 259 --- .../Internals/TrueType.PointerSpan.cs | 443 ------ .../Internals/TrueType.Tables.cs | 1391 ----------------- .../ManagedFontAtlas/Internals/TrueType.cs | 135 -- .../ManagedFontAtlas/SafeFontConfig.cs | 306 ---- Dalamud/Interface/UiBuilder.cs | 182 +-- Dalamud/Interface/Utility/ImGuiHelpers.cs | 243 +-- 44 files changed, 1499 insertions(+), 7943 deletions(-) delete mode 100644 Dalamud/Interface/GameFonts/FdtFileView.cs delete mode 100644 Dalamud/Interface/GameFonts/GameFontFamilyAndSizeAttribute.cs create mode 100644 Dalamud/Interface/GameFonts/GameFontManager.cs delete mode 100644 Dalamud/Interface/Internal/Windows/Data/Widgets/GamePrebakedFontsTestWidget.cs delete mode 100644 Dalamud/Interface/ManagedFontAtlas/FontAtlasAutoRebuildMode.cs delete mode 100644 Dalamud/Interface/ManagedFontAtlas/FontAtlasBuildStep.cs delete mode 100644 Dalamud/Interface/ManagedFontAtlas/FontAtlasBuildStepDelegate.cs delete mode 100644 Dalamud/Interface/ManagedFontAtlas/FontAtlasBuildToolkitUtilities.cs delete mode 100644 Dalamud/Interface/ManagedFontAtlas/IFontAtlas.cs delete mode 100644 Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkit.cs delete mode 100644 Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPostBuild.cs delete mode 100644 Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPostPromotion.cs delete mode 100644 Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPreBuild.cs delete mode 100644 Dalamud/Interface/ManagedFontAtlas/IFontHandle.cs delete mode 100644 Dalamud/Interface/ManagedFontAtlas/Internals/DelegateFontHandle.cs delete mode 100644 Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs delete mode 100644 Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs delete mode 100644 Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs delete mode 100644 Dalamud/Interface/ManagedFontAtlas/Internals/GamePrebakedFontHandle.cs delete mode 100644 Dalamud/Interface/ManagedFontAtlas/Internals/IFontHandleManager.cs delete mode 100644 Dalamud/Interface/ManagedFontAtlas/Internals/IFontHandleSubstance.cs delete mode 100644 Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.Common.cs delete mode 100644 Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.Enums.cs delete mode 100644 Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.Files.cs delete mode 100644 Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.GposGsub.cs delete mode 100644 Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.PointerSpan.cs delete mode 100644 Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.Tables.cs delete mode 100644 Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.cs delete mode 100644 Dalamud/Interface/ManagedFontAtlas/SafeFontConfig.cs diff --git a/Dalamud/Configuration/Internal/DalamudConfiguration.cs b/Dalamud/Configuration/Internal/DalamudConfiguration.cs index 66c2745c5..76c8f3603 100644 --- a/Dalamud/Configuration/Internal/DalamudConfiguration.cs +++ b/Dalamud/Configuration/Internal/DalamudConfiguration.cs @@ -148,9 +148,12 @@ internal sealed class DalamudConfiguration : IServiceType, IDisposable public bool UseAxisFontsFromGame { get; set; } = false; /// - /// Gets or sets the gamma value to apply for Dalamud fonts. Do not use. + /// 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. /// - [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 deleted file mode 100644 index 896a6dbb4..000000000 --- a/Dalamud/Interface/GameFonts/FdtFileView.cs +++ /dev/null @@ -1,159 +0,0 @@ -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 6e66cf19b..dd78baf87 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 +public enum GameFontFamilyAndSize : int { /// /// Placeholder meaning unused. @@ -15,7 +15,6 @@ public enum GameFontFamilyAndSize /// /// 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, /// @@ -23,7 +22,6 @@ public enum GameFontFamilyAndSize /// /// 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, /// @@ -31,7 +29,6 @@ public enum GameFontFamilyAndSize /// /// 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, /// @@ -39,7 +36,6 @@ public enum GameFontFamilyAndSize /// /// 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, /// @@ -47,7 +43,6 @@ public enum GameFontFamilyAndSize /// /// 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, /// @@ -55,7 +50,6 @@ public enum GameFontFamilyAndSize /// /// 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, /// @@ -63,7 +57,6 @@ public enum GameFontFamilyAndSize /// /// 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, /// @@ -71,7 +64,6 @@ public enum GameFontFamilyAndSize /// /// 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, /// @@ -79,7 +71,6 @@ public enum GameFontFamilyAndSize /// /// Serif font. Contains mostly numbers. Used in game for flying texts. /// - [GameFontFamilyAndSize("common/font/Jupiter_45.fdt", "common/font/font{0}.tex", -2)] Jupiter45, /// @@ -87,7 +78,6 @@ public enum GameFontFamilyAndSize /// /// 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, /// @@ -95,7 +85,6 @@ public enum GameFontFamilyAndSize /// /// Serif font. Contains mostly numbers. Used in game for flying texts. /// - [GameFontFamilyAndSize("common/font/Jupiter_90.fdt", "common/font/font{0}.tex", -4)] Jupiter90, /// @@ -103,7 +92,6 @@ public enum GameFontFamilyAndSize /// /// 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, /// @@ -111,7 +99,6 @@ public enum GameFontFamilyAndSize /// /// 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, /// @@ -119,7 +106,6 @@ public enum GameFontFamilyAndSize /// /// 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, /// @@ -127,7 +113,6 @@ public enum GameFontFamilyAndSize /// /// Horizontally wide. Contains mostly ASCII range. /// - [GameFontFamilyAndSize("common/font/MiedingerMid_10.fdt", "common/font/font{0}.tex", -1)] MiedingerMid10, /// @@ -135,7 +120,6 @@ public enum GameFontFamilyAndSize /// /// Horizontally wide. Contains mostly ASCII range. /// - [GameFontFamilyAndSize("common/font/MiedingerMid_12.fdt", "common/font/font{0}.tex", -1)] MiedingerMid12, /// @@ -143,7 +127,6 @@ public enum GameFontFamilyAndSize /// /// Horizontally wide. Contains mostly ASCII range. /// - [GameFontFamilyAndSize("common/font/MiedingerMid_14.fdt", "common/font/font{0}.tex", -1)] MiedingerMid14, /// @@ -151,7 +134,6 @@ public enum GameFontFamilyAndSize /// /// Horizontally wide. Contains mostly ASCII range. /// - [GameFontFamilyAndSize("common/font/MiedingerMid_18.fdt", "common/font/font{0}.tex", -1)] MiedingerMid18, /// @@ -159,7 +141,6 @@ public enum GameFontFamilyAndSize /// /// Horizontally wide. Contains mostly ASCII range. /// - [GameFontFamilyAndSize("common/font/MiedingerMid_36.fdt", "common/font/font{0}.tex", -2)] MiedingerMid36, /// @@ -167,7 +148,6 @@ public enum GameFontFamilyAndSize /// /// Horizontally narrow. Contains mostly ASCII range. Used for addon titles. /// - [GameFontFamilyAndSize("common/font/TrumpGothic_184.fdt", "common/font/font{0}.tex", -1)] TrumpGothic184, /// @@ -175,7 +155,6 @@ public enum GameFontFamilyAndSize /// /// Horizontally narrow. Contains mostly ASCII range. Used for addon titles. /// - [GameFontFamilyAndSize("common/font/TrumpGothic_23.fdt", "common/font/font{0}.tex", -1)] TrumpGothic23, /// @@ -183,7 +162,6 @@ public enum GameFontFamilyAndSize /// /// Horizontally narrow. Contains mostly ASCII range. Used for addon titles. /// - [GameFontFamilyAndSize("common/font/TrumpGothic_34.fdt", "common/font/font{0}.tex", -1)] TrumpGothic34, /// @@ -191,6 +169,5 @@ public enum GameFontFamilyAndSize /// /// 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 deleted file mode 100644 index f5260e4bc..000000000 --- a/Dalamud/Interface/GameFonts/GameFontFamilyAndSizeAttribute.cs +++ /dev/null @@ -1,37 +0,0 @@ -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 77461aa0a..d71e725c5 100644 --- a/Dalamud/Interface/GameFonts/GameFontHandle.cs +++ b/Dalamud/Interface/GameFonts/GameFontHandle.cs @@ -1,76 +1,75 @@ +using System; using System.Numerics; -using Dalamud.Interface.ManagedFontAtlas; -using Dalamud.Interface.ManagedFontAtlas.Internals; - using ImGuiNET; namespace Dalamud.Interface.GameFonts; /// -/// ABI-compatible wrapper for . +/// Prepare and keep game font loaded for use in OnDraw. /// -public sealed class GameFontHandle : IFontHandle +public class GameFontHandle : IDisposable { - private readonly IFontHandle.IInternal fontHandle; - private readonly FontAtlasFactory fontAtlasFactory; + private readonly GameFontManager manager; + private readonly GameFontStyle fontStyle; /// /// Initializes a new instance of the class. /// - /// The wrapped . - /// An instance of . - internal GameFontHandle(IFontHandle.IInternal fontHandle, FontAtlasFactory fontAtlasFactory) + /// GameFontManager instance. + /// Font to use. + internal GameFontHandle(GameFontManager manager, GameFontStyle font) { - this.fontHandle = fontHandle; - this.fontAtlasFactory = fontAtlasFactory; + this.manager = manager; + this.fontStyle = font; } - /// - 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 the font style. Only applicable for . + /// Gets the font style. /// - [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; + public GameFontStyle Style => this.fontStyle; /// - /// Gets the relevant .
- ///
- /// Only applicable for game fonts. Otherwise it will throw. + /// Gets a value indicating whether this font is ready for use. ///
- [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(); + public bool Available + { + get + { + unsafe + { + return this.manager.GetFont(this.fontStyle).GetValueOrDefault(null).NativePtr != null; + } + } + } /// - /// Creates a new .
- ///
- /// Only applicable for game fonts. Otherwise it will throw. + /// Gets the font. + ///
+ public ImFontPtr ImFont => this.manager.GetFont(this.fontStyle).Value; + + /// + /// Gets the FdtReader. + /// + public FdtReader FdtReader => this.manager.GetFdtReader(this.fontStyle.FamilyAndSize); + + /// + /// Creates a new GameFontLayoutPlan.Builder. /// /// Text. /// A new builder for GameFontLayoutPlan. - [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); + public GameFontLayoutPlan.Builder LayoutBuilder(string text) + { + return new GameFontLayoutPlan.Builder(this.ImFont, this.FdtReader, text); + } + + /// + public void Dispose() => this.manager.DecreaseFontRef(this.fontStyle); /// /// 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) @@ -94,7 +93,6 @@ public sealed class GameFontHandle : IFontHandle ///
/// 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); @@ -106,7 +104,6 @@ public sealed class GameFontHandle : IFontHandle /// 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 new file mode 100644 index 000000000..b3454e085 --- /dev/null +++ b/Dalamud/Interface/GameFonts/GameFontManager.cs @@ -0,0 +1,507 @@ +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 fbaf9de07..946473df4 100644 --- a/Dalamud/Interface/GameFonts/GameFontStyle.cs +++ b/Dalamud/Interface/GameFonts/GameFontStyle.cs @@ -64,7 +64,7 @@ public struct GameFontStyle /// public float SizePt { - readonly get => this.SizePx * 3 / 4; + get => this.SizePx * 3 / 4; set => this.SizePx = value * 4 / 3; } @@ -73,14 +73,14 @@ public struct GameFontStyle /// public float BaseSkewStrength { - readonly get => this.SkewStrength * this.BaseSizePx / this.SizePx; + get => this.SkewStrength * this.BaseSizePx / this.SizePx; set => this.SkewStrength = value * this.SizePx / this.BaseSizePx; } /// /// Gets the font family. /// - public readonly GameFontFamily Family => this.FamilyAndSize switch + public 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 readonly GameFontFamilyAndSize FamilyWithMinimumSize => this.Family switch + public 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 readonly float BaseSizePt => this.FamilyAndSize switch + public 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 readonly float BaseSizePx => this.BaseSizePt * 4 / 3; + public float BaseSizePx => this.BaseSizePt * 4 / 3; /// /// Gets or sets a value indicating whether this font is bold. /// public bool Bold { - readonly get => this.Weight > 0f; + get => this.Weight > 0f; set => this.Weight = value ? 1f : 0f; } @@ -174,8 +174,8 @@ public struct GameFontStyle /// public bool Italic { - readonly get => this.SkewStrength != 0; - set => this.SkewStrength = value ? this.SizePx / 6 : 0; + get => this.SkewStrength != 0; + set => this.SkewStrength = value ? this.SizePx / 7 : 0; } /// @@ -233,26 +233,13 @@ 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 readonly int CalculateBaseWidthAdjustment(in FdtReader.FontTableHeader header, in FdtReader.FontTableEntry glyph) + public int CalculateBaseWidthAdjustment(in FdtReader.FontTableHeader header, in FdtReader.FontTableEntry glyph) { var widthDelta = this.Weight; switch (this.BaseSkewStrength) @@ -276,11 +263,11 @@ public struct GameFontStyle /// Font information. /// Glyph. /// Width adjustment in pixel unit. - public readonly int CalculateBaseWidthAdjustment(FdtReader reader, FdtReader.FontTableEntry glyph) => + public int CalculateBaseWidthAdjustment(FdtReader reader, FdtReader.FontTableEntry glyph) => this.CalculateBaseWidthAdjustment(reader.FontHeader, glyph); /// - public override readonly string ToString() + public override 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 28a9075bd..e030b4e50 100644 --- a/Dalamud/Interface/Internal/DalamudIme.cs +++ b/Dalamud/Interface/Internal/DalamudIme.cs @@ -11,7 +11,6 @@ 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; @@ -197,9 +196,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 60c1f9957..95415659b 100644 --- a/Dalamud/Interface/Internal/DalamudInterface.cs +++ b/Dalamud/Interface/Internal/DalamudInterface.cs @@ -21,7 +21,6 @@ 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; @@ -94,8 +93,7 @@ internal class DalamudInterface : IDisposable, IServiceType private DalamudInterface( Dalamud dalamud, DalamudConfiguration configuration, - FontAtlasFactory fontAtlasFactory, - InterfaceManager interfaceManager, + InterfaceManager.InterfaceManagerWithScene interfaceManagerWithScene, PluginImageCache pluginImageCache, DalamudAssetManager dalamudAssetManager, Game.Framework framework, @@ -105,7 +103,7 @@ internal class DalamudInterface : IDisposable, IServiceType { this.dalamud = dalamud; this.configuration = configuration; - this.interfaceManager = interfaceManager; + this.interfaceManager = interfaceManagerWithScene.Manager; this.WindowSystem = new WindowSystem("DalamudCore"); @@ -124,14 +122,10 @@ internal class DalamudInterface : IDisposable, IServiceType clientState, configuration, dalamudAssetManager, - fontAtlasFactory, framework, gameGui, titleScreenMenu) { IsOpen = false }; - this.changelogWindow = new ChangelogWindow( - this.titleScreenMenuWindow, - fontAtlasFactory, - dalamudAssetManager) { IsOpen = false }; + this.changelogWindow = new ChangelogWindow(this.titleScreenMenuWindow) { IsOpen = false }; this.profilerWindow = new ProfilerWindow() { IsOpen = false }; this.branchSwitcherWindow = new BranchSwitcherWindow() { IsOpen = false }; this.hitchSettingsWindow = new HitchSettingsWindow() { IsOpen = false }; @@ -213,7 +207,6 @@ 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 3e004727a..48157fa86 100644 --- a/Dalamud/Interface/Internal/InterfaceManager.cs +++ b/Dalamud/Interface/Internal/InterfaceManager.cs @@ -1,10 +1,13 @@ +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.Threading.Tasks; +using System.Text; +using System.Text.Unicode; +using System.Threading; using Dalamud.Configuration.Internal; using Dalamud.Game; @@ -16,13 +19,10 @@ 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.Plugin.Internal; -using Dalamud.Plugin.Internal.Types; +using Dalamud.Storage.Assets; using Dalamud.Utility; using Dalamud.Utility.Timing; using ImGuiNET; @@ -64,9 +64,11 @@ internal class InterfaceManager : IDisposable, IServiceType /// public const float DefaultFontSizePx = (DefaultFontSizePt * 4.0f) / 3.0f; - private const int NonMainThreadFontAccessWarningCheckInterval = 10000; - private static readonly ConditionalWeakTable NonMainThreadFontAccessWarning = new(); - private static long nextNonMainThreadFontAccessWarningCheck; + 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 readonly List deferredDisposeTextures = new(); @@ -79,28 +81,28 @@ internal class InterfaceManager : IDisposable, IServiceType [ServiceManager.ServiceDependency] private readonly DalamudIme dalamudIme = Service.Get(); - private readonly SwapChainVtableResolver address = new(); + private readonly ManualResetEvent fontBuildSignal; + private readonly SwapChainVtableResolver address; 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)] @@ -115,46 +117,43 @@ 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; + public event Action ResizeBuffers; + + /// + /// Gets or sets an action that is executed right before fonts are rebuilt. + /// + public event Action BuildFonts; /// /// 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.
- /// Accessing this static property outside of the main thread is dangerous and not supported. + /// Gets the default ImGui font. ///
- public static ImFontPtr DefaultFont => WhenFontsReady().defaultFontHandle!.ImFont.OrElse(ImGui.GetIO().FontDefault); + public static ImFontPtr DefaultFont { get; private set; } /// - /// Gets an included FontAwesome icon font.
- /// Accessing this static property outside of the main thread is dangerous and not supported. + /// Gets an included FontAwesome icon font. ///
- public static ImFontPtr IconFont => WhenFontsReady().iconFontHandle!.ImFont.OrElse(ImGui.GetIO().FontDefault); + public static ImFontPtr IconFont { get; private set; } /// - /// Gets an included monospaced font.
- /// Accessing this static property outside of the main thread is dangerous and not supported. + /// Gets an included monospaced font. ///
- public static ImFontPtr MonoFont => WhenFontsReady().monoFontHandle!.ImFont.OrElse(ImGui.GetIO().FontDefault); + public static ImFontPtr MonoFont { get; private set; } /// /// 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. /// @@ -179,6 +178,11 @@ 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. /// @@ -190,56 +194,49 @@ internal class InterfaceManager : IDisposable, IServiceType public bool IsDispatchingEvents { get; set; } = true; /// - /// Gets a value indicating the native handle of the game main window. + /// Gets or sets a value indicating whether to override configuration for UseAxis. /// - 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; - } - } + public bool? UseAxisOverride { get; set; } = null; /// - /// Gets the font build task. + /// Gets a value indicating whether to use AXIS fonts. /// - public Task FontBuildTask => WhenFontsReady().dalamudAtlas!.BuildTask; + 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; } /// /// Dispose of managed and unmanaged resources. /// public void Dispose() { - 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.framework.RunOnFrameworkThread(() => { this.setCursorHook.Dispose(); this.presentHook?.Dispose(); this.resizeBuffersHook?.Dispose(); - } + }).Wait(); + + this.wndProcHookManager.PreWndProc -= this.WndProcHookManagerOnPreWndProc; + this.scene?.Dispose(); } #nullable enable @@ -379,8 +376,93 @@ 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"); - this.dalamudAtlas?.BuildFontsAsync(); + + // 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); } /// @@ -404,11 +486,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, MemorySegmentGroup.Local); + var memInfo = dxgiAdapter.QueryVideoMemoryInfo(0, SharpDX.DXGI.MemorySegmentGroup.Local); return (memInfo.CurrentUsage, memInfo.CurrentReservation); } catch @@ -434,65 +516,20 @@ internal class InterfaceManager : IDisposable, IServiceType /// Value. internal void SetImmersiveMode(bool enabled) { - 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(); + 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)); } - private static InterfaceManager WhenFontsReady() + private static void ShowFontError(string path) { - 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(); + 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"); } private void InitScene(IntPtr swapChain) @@ -509,7 +546,7 @@ internal class InterfaceManager : IDisposable, IServiceType Service.ProvideException(ex); Log.Error(ex, "Could not load ImGui dependencies."); - var res = User32.MessageBox( + var res = PInvoke.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", @@ -541,7 +578,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(); } } @@ -586,6 +623,8 @@ internal class InterfaceManager : IDisposable, IServiceType ImGui.GetIO().FontGlobalScale = configuration.GlobalUiScale; + this.SetupFonts(); + if (!configuration.IsDocking) { ImGui.GetIO().ConfigFlags &= ~ImGuiConfigFlags.DockingEnable; @@ -636,34 +675,26 @@ 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); - RenderImGui(this.scene!); + this.RenderImGui(); this.DisposeTextures(); return pRes; } - RenderImGui(this.scene!); + this.RenderImGui(); this.DisposeTextures(); - return this.presentHook!.Original(swapChain, syncInterval, presentFlags); + return this.presentHook.Original(swapChain, syncInterval, presentFlags); } private void DisposeTextures() @@ -680,73 +711,471 @@ internal class InterfaceManager : IDisposable, IServiceType } } - [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) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void RenderImGui() { - this.dalamudAtlas = fontAtlasFactory - .CreateFontAtlas(nameof(InterfaceManager), FontAtlasAutoRebuildMode.Disable); - using (this.dalamudAtlas.SuppressAutoRebuild()) + // 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.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; + return; } - // This will wait for scene on its own. We just wait for this.dalamudAtlas.BuildTask in this.InitScene. - _ = this.dalamudAtlas.BuildFontsAsync(false); + ImGui.GetIO().ConfigFlags |= ImGuiConfigFlags.ViewportsEnable; + } - this.address.Setup(sigScanner); + /// + /// 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(); try { - if (Service.Get().WindowIsImmersive) - this.SetImmersiveMode(true); + 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; } - catch (Exception ex) + finally { - Log.Error(ex, "Could not enable immersive mode"); + if (fontConfig.NativePtr != null) + fontConfig.Destroy(); + + foreach (var garbage in garbageList) + garbage.Free(); } + } - this.presentHook = Hook.FromAddress(this.address.Present, this.PresentDetour); - this.resizeBuffersHook = Hook.FromAddress(this.address.ResizeBuffers, this.ResizeBuffersDetour); + [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); - 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}"); + if (pid == Environment.ProcessId && User32.IsWindowVisible(this.GameWindowHandle)) + break; + } - this.setCursorHook.Enable(); - this.presentHook.Enable(); - this.resizeBuffersHook.Enable(); + 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; } private IntPtr ResizeBuffersDetour(IntPtr swapChain, uint bufferCount, uint width, uint height, uint newFormat, uint swapChainFlags) @@ -777,17 +1206,14 @@ internal class InterfaceManager : IDisposable, IServiceType private IntPtr SetCursorDetour(IntPtr hCursor) { - if (this.lastWantCapture && (!this.scene?.IsImGuiCursor(hCursor) ?? false) && this.OverrideGameCursor) + if (this.lastWantCapture == true && (!this.scene?.IsImGuiCursor(hCursor) ?? false) && this.OverrideGameCursor) return IntPtr.Zero; - return this.setCursorHook.IsDisposed - ? User32.SetCursor(new(hCursor, false)).DangerousGetHandle() - : this.setCursorHook.Original(hCursor); + return this.setCursorHook.IsDisposed ? User32.SetCursor(new User32.SafeCursorHandle(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(); @@ -795,21 +1221,18 @@ 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 (io.WantTextInput) + if (ImGui.GetIO().WantTextInput) { keyState.ClearAll(); } // TODO: mouse state? - var gamepadEnabled = (io.BackendFlags & ImGuiBackendFlags.HasGamepad) > 0; + var gamepadEnabled = (ImGui.GetIO().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). @@ -817,12 +1240,12 @@ internal class InterfaceManager : IDisposable, IServiceType && gamepadState.Raw(GamepadButtons.L1) > 0 && gamepadState.Pressed(GamepadButtons.L3) > 0) { - io.ConfigFlags ^= ImGuiConfigFlags.NavEnableGamepad; + ImGui.GetIO().ConfigFlags ^= ImGuiConfigFlags.NavEnableGamepad; gamepadState.NavEnableGamepad ^= true; dalamudInterface.ToggleGamepadModeNotifierWindow(); } - if (gamepadEnabled && (io.ConfigFlags & ImGuiConfigFlags.NavEnableGamepad) > 0) + if (gamepadEnabled && (ImGui.GetIO().ConfigFlags & ImGuiConfigFlags.NavEnableGamepad) > 0) { var northButton = gamepadState.Raw(GamepadButtons.North) != 0; var eastButton = gamepadState.Raw(GamepadButtons.East) != 0; @@ -841,6 +1264,7 @@ 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); @@ -888,10 +1312,7 @@ internal class InterfaceManager : IDisposable, IServiceType var snap = ImGuiManagedAsserts.GetSnapshot(); if (this.IsDispatchingEvents) - { - using (this.defaultFontHandle?.Push()) - this.Draw?.Invoke(); - } + this.Draw?.Invoke(); ImGuiManagedAsserts.ReportProblems("Dalamud Core", snap); @@ -918,4 +1339,123 @@ 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 ae59db36a..b9e7ab686 100644 --- a/Dalamud/Interface/Internal/Windows/ChangelogWindow.cs +++ b/Dalamud/Interface/Internal/Windows/ChangelogWindow.cs @@ -1,3 +1,4 @@ +using System.IO; using System.Linq; using System.Numerics; @@ -6,8 +7,6 @@ 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; @@ -32,14 +31,8 @@ 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)) { @@ -53,36 +46,27 @@ 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. - /// An instance of . - /// An instance of . - public ChangelogWindow( - TitleScreenMenuWindow tsmWindow, - FontAtlasFactory fontAtlasFactory, - DalamudAssetManager assets) + public ChangelogWindow(TitleScreenMenuWindow tsmWindow) : 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()) - _ = this.bannerFont.Value; + Service.GetAsync().ContinueWith(t => this.MakeFont(t.Result)); } private enum State @@ -113,12 +97,20 @@ internal sealed class ChangelogWindow : Window, IDisposable Service.Get().SetCreditsDarkeningAnimation(true); this.tsmWindow.AllowDrawing = false; - _ = this.bannerFont; + this.MakeFont(Service.Get()); 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(); } @@ -194,7 +186,10 @@ 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))) - ImGui.Image(this.logoTexture.Value.ImGuiHandle, logoSize); + { + this.logoTexture ??= Service.Get().GetDalamudTextureWrap(DalamudAsset.Logo); + ImGui.Image(this.logoTexture.ImGuiHandle, logoSize); + } } ImGui.SameLine(); @@ -210,7 +205,7 @@ internal sealed class ChangelogWindow : Window, IDisposable using (ImRaii.PushStyle(ImGuiStyleVar.Alpha, Math.Clamp(this.windowFade.EasedPoint.X - 1f, 0f, 1f))) { - using var font = this.bannerFont.Value.Push(); + using var font = ImRaii.PushFont(this.bannerFont!.ImFont); switch (this.state) { @@ -280,11 +275,9 @@ 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.Value.Width); - ImGui.Image( - this.apiBumpExplainerTexture.Value.ImGuiHandle, - this.apiBumpExplainerTexture.Value.Size); + + ImGuiHelpers.CenterCursorFor(this.apiBumpExplainerTexture!.Width); + ImGui.Image(this.apiBumpExplainerTexture.ImGuiHandle, this.apiBumpExplainerTexture.Size); DrawNextButton(State.Links); break; @@ -384,4 +377,7 @@ 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 951d3d91c..20c3d6d01 100644 --- a/Dalamud/Interface/Internal/Windows/Data/DataWindow.cs +++ b/Dalamud/Interface/Internal/Windows/Data/DataWindow.cs @@ -6,8 +6,6 @@ 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; @@ -16,7 +14,7 @@ namespace Dalamud.Interface.Internal.Windows.Data; /// /// Class responsible for drawing the data/debug window. /// -internal class DataWindow : Window, IDisposable +internal class DataWindow : Window { private readonly IDataWindowWidget[] modules = { @@ -36,7 +34,6 @@ internal class DataWindow : Window, IDisposable new FlyTextWidget(), new FontAwesomeTestWidget(), new GameInventoryTestWidget(), - new GamePrebakedFontsTestWidget(), new GamepadWidget(), new GaugeWidget(), new HookWidget(), @@ -79,9 +76,6 @@ internal class DataWindow : Window, IDisposable 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 deleted file mode 100644 index dba293e8b..000000000 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/GamePrebakedFontsTestWidget.cs +++ /dev/null @@ -1,213 +0,0 @@ -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 027e1a571..7d4489f8d 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,7 +19,14 @@ namespace Dalamud.Interface.Internal.Windows.Settings; /// internal class SettingsWindow : Window { - private SettingsTab[]? tabs; + private readonly SettingsTab[] tabs = + { + new SettingsTabGeneral(), + new SettingsTabLook(), + new SettingsTabDtr(), + new SettingsTabExperimental(), + new SettingsTabAbout(), + }; private string searchInput = string.Empty; @@ -42,15 +49,6 @@ 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(); @@ -66,12 +64,15 @@ internal class SettingsWindow : Window { var configuration = Service.Get(); var interfaceManager = Service.Get(); - var fontAtlasFactory = Service.Get(); - var rebuildFont = fontAtlasFactory.UseAxis != configuration.UseAxisFontsFromGame; + var rebuildFont = + ImGui.GetIO().FontGlobalScale != configuration.GlobalUiScale || + interfaceManager.FontGamma != configuration.FontGammaLevel || + interfaceManager.UseAxis != configuration.UseAxisFontsFromGame; ImGui.GetIO().FontGlobalScale = configuration.GlobalUiScale; - fontAtlasFactory.UseAxisOverride = null; + interfaceManager.FontGammaOverride = null; + interfaceManager.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 8714fd666..5b6f6b02f 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.Diagnostics; +using System; +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,6 +15,7 @@ using Dalamud.Storage.Assets; using Dalamud.Utility; using FFXIVClientStructs.FFXIV.Client.Game.UI; using ImGuiNET; +using ImGuiScene; namespace Dalamud.Interface.Internal.Windows.Settings.Tabs; @@ -172,21 +173,16 @@ 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 IFontHandle? thankYouFont; + private GameFontHandle? thankYouFont; public SettingsTabAbout() { this.creditsThrottler = new(); - - this.privateAtlas = Service - .Get() - .CreateFontAtlas(nameof(SettingsTabAbout), FontAtlasAutoRebuildMode.Async); } public override SettingsEntry[] Entries { get; } = { }; @@ -211,7 +207,11 @@ Contribute at: https://github.com/goatcorp/Dalamud this.creditsThrottler.Restart(); - this.thankYouFont ??= this.privateAtlas.NewGameFontHandle(new(GameFontFamilyAndSize.TrumpGothic34)); + if (this.thankYouFont == null) + { + var gfm = Service.Get(); + this.thankYouFont = gfm.NewFontRef(new GameFontStyle(GameFontFamilyAndSize.TrumpGothic34)); + } this.resetNow = true; @@ -269,12 +269,14 @@ Contribute at: https://github.com/goatcorp/Dalamud if (this.thankYouFont != null) { - using var fontPush = this.thankYouFont.Push(); + ImGui.PushFont(this.thankYouFont.ImFont); 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); @@ -303,5 +305,9 @@ Contribute at: https://github.com/goatcorp/Dalamud /// /// Disposes of managed and unmanaged resources. /// - public override void Dispose() => this.privateAtlas.Dispose(); + public override void Dispose() + { + this.logoTexture?.Dispose(); + this.thankYouFont?.Dispose(); + } } diff --git a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabLook.cs b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabLook.cs index 5293e13c4..02e8ce789 100644 --- a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabLook.cs +++ b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabLook.cs @@ -1,14 +1,12 @@ 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; @@ -30,6 +28,7 @@ public class SettingsTabLook : SettingsTab }; private float globalUiScale; + private float fontGamma; public override SettingsEntry[] Entries { get; } = { @@ -42,8 +41,9 @@ public class SettingsTabLook : SettingsTab (v, c) => c.UseAxisFontsFromGame = v, v => { - Service.Get().UseAxisOverride = v; - Service.Get().RebuildFonts(); + var im = Service.Get(); + im.UseAxisOverride = v; + im.RebuildFonts(); }), new GapSettingsEntry(5, true), @@ -145,7 +145,6 @@ 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")); @@ -165,19 +164,6 @@ 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)) { @@ -188,25 +174,33 @@ public class SettingsTabLook : SettingsTab ImGuiHelpers.SafeTextColoredWrapped(ImGuiColors.DalamudGrey, Loc.Localize("DalamudSettingsGlobalUiScaleHint", "Scale text in all XIVLauncher UI elements - this is useful for 4K displays.")); - if (fontBuildTask.IsFaulted || fontBuildTask.IsCanceled) + ImGuiHelpers.ScaledDummy(5); + + ImGui.AlignTextToFramePadding(); + ImGui.Text(Loc.Localize("DalamudSettingsFontGamma", "Font Gamma")); + ImGui.SameLine(); + if (ImGui.Button(Loc.Localize("DalamudSettingsIndividualConfigResetToDefaultValue", "Reset") + "##DalamudSettingsFontGammaReset")) { - 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()); - } + this.fontGamma = 1.4f; + interfaceManager.FontGammaOverride = this.fontGamma; + interfaceManager.RebuildFonts(); } + 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 9c385a99c..42bca89ff 100644 --- a/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs +++ b/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs @@ -7,14 +7,11 @@ 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; @@ -30,17 +27,16 @@ 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; @@ -52,7 +48,6 @@ 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 . @@ -60,7 +55,6 @@ internal class TitleScreenMenuWindow : Window, IDisposable ClientState clientState, DalamudConfiguration configuration, DalamudAssetManager dalamudAssetManager, - FontAtlasFactory fontAtlasFactory, Framework framework, GameGui gameGui, TitleScreenMenu titleScreenMenu) @@ -71,6 +65,7 @@ internal class TitleScreenMenuWindow : Window, IDisposable { this.clientState = clientState; this.configuration = configuration; + this.framework = framework; this.gameGui = gameGui; this.titleScreenMenu = titleScreenMenu; @@ -82,25 +77,9 @@ 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 @@ -115,9 +94,6 @@ internal class TitleScreenMenuWindow : Window, IDisposable /// public bool AllowDrawing { get; set; } = true; - /// - public void Dispose() => this.scopedFinalizer.Dispose(); - /// public override void PreDraw() { @@ -133,6 +109,12 @@ internal class TitleScreenMenuWindow : Window, IDisposable base.PostDraw(); } + /// + public void Dispose() + { + this.framework.Update -= this.FrameworkOnUpdate; + } + /// public override void Draw() { @@ -264,12 +246,33 @@ 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) { - using var fontScopeDispose = this.myFontHandle.Value.Push(); + 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); var scale = ImGui.GetIO().FontGlobalScale; @@ -380,6 +383,8 @@ internal class TitleScreenMenuWindow : Window, IDisposable initialCursor.Y += entry.Texture.Height * scale; ImGui.SetCursorPos(initialCursor); + ImGui.PopFont(); + return isHover; } @@ -396,6 +401,4 @@ 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 deleted file mode 100644 index 50e591390..000000000 --- a/Dalamud/Interface/ManagedFontAtlas/FontAtlasAutoRebuildMode.cs +++ /dev/null @@ -1,22 +0,0 @@ -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 deleted file mode 100644 index 345ab729d..000000000 --- a/Dalamud/Interface/ManagedFontAtlas/FontAtlasBuildStep.cs +++ /dev/null @@ -1,38 +0,0 @@ -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 deleted file mode 100644 index 4f5b34061..000000000 --- a/Dalamud/Interface/ManagedFontAtlas/FontAtlasBuildStepDelegate.cs +++ /dev/null @@ -1,15 +0,0 @@ -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 deleted file mode 100644 index 586887a3b..000000000 --- a/Dalamud/Interface/ManagedFontAtlas/FontAtlasBuildToolkitUtilities.cs +++ /dev/null @@ -1,133 +0,0 @@ -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 deleted file mode 100644 index ec3e66e9a..000000000 --- a/Dalamud/Interface/ManagedFontAtlas/IFontAtlas.cs +++ /dev/null @@ -1,141 +0,0 @@ -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 deleted file mode 100644 index 4b016bbb2..000000000 --- a/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkit.cs +++ /dev/null @@ -1,67 +0,0 @@ -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 deleted file mode 100644 index 3c14197e0..000000000 --- a/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPostBuild.cs +++ /dev/null @@ -1,26 +0,0 @@ -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 deleted file mode 100644 index 8c3c91624..000000000 --- a/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPostPromotion.cs +++ /dev/null @@ -1,33 +0,0 @@ -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 deleted file mode 100644 index cb8a27a54..000000000 --- a/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPreBuild.cs +++ /dev/null @@ -1,186 +0,0 @@ -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 deleted file mode 100644 index 854594663..000000000 --- a/Dalamud/Interface/ManagedFontAtlas/IFontHandle.cs +++ /dev/null @@ -1,42 +0,0 @@ -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 deleted file mode 100644 index f0ed09155..000000000 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/DelegateFontHandle.cs +++ /dev/null @@ -1,334 +0,0 @@ -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 deleted file mode 100644 index e73ea7548..000000000 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs +++ /dev/null @@ -1,682 +0,0 @@ -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 deleted file mode 100644 index 5656fc673..000000000 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs +++ /dev/null @@ -1,726 +0,0 @@ -// #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 deleted file mode 100644 index 358ccd845..000000000 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs +++ /dev/null @@ -1,368 +0,0 @@ -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 deleted file mode 100644 index 99c817a91..000000000 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/GamePrebakedFontHandle.cs +++ /dev/null @@ -1,857 +0,0 @@ -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 deleted file mode 100644 index 93c688608..000000000 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/IFontHandleManager.cs +++ /dev/null @@ -1,32 +0,0 @@ -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 deleted file mode 100644 index f6c5c6591..000000000 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/IFontHandleSubstance.cs +++ /dev/null @@ -1,54 +0,0 @@ -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 deleted file mode 100644 index 8e7149853..000000000 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.Common.cs +++ /dev/null @@ -1,203 +0,0 @@ -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 deleted file mode 100644 index f6a653a51..000000000 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.Enums.cs +++ /dev/null @@ -1,84 +0,0 @@ -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 deleted file mode 100644 index 3d89dd806..000000000 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.Files.cs +++ /dev/null @@ -1,148 +0,0 @@ -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 deleted file mode 100644 index d200de47b..000000000 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.GposGsub.cs +++ /dev/null @@ -1,259 +0,0 @@ -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 deleted file mode 100644 index c91df4ff2..000000000 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.PointerSpan.cs +++ /dev/null @@ -1,443 +0,0 @@ -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 deleted file mode 100644 index 80cf4b7da..000000000 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.Tables.cs +++ /dev/null @@ -1,1391 +0,0 @@ -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 deleted file mode 100644 index 1d437d56d..000000000 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.cs +++ /dev/null @@ -1,135 +0,0 @@ -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 deleted file mode 100644 index cb7f7c65a..000000000 --- a/Dalamud/Interface/ManagedFontAtlas/SafeFontConfig.cs +++ /dev/null @@ -1,306 +0,0 @@ -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 a477ec09e..dd2e5bad3 100644 --- a/Dalamud/Interface/UiBuilder.cs +++ b/Dalamud/Interface/UiBuilder.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Diagnostics; using System.Threading.Tasks; @@ -11,8 +12,6 @@ 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; @@ -31,13 +30,11 @@ public sealed class UiBuilder : IDisposable private readonly HitchDetector hitchDetector; private readonly string namespaceName; private readonly InterfaceManager interfaceManager = Service.Get(); - private readonly Framework framework = Service.Get(); + private readonly GameFontManager gameFontManager = Service.Get(); [ServiceManager.ServiceDependency] private readonly DalamudConfiguration configuration = Service.Get(); - private readonly DisposeSafety.ScopedFinalizer scopedFinalizer = new(); - private bool hasErrorWindow = false; private bool lastFrameUiHideState = false; @@ -48,32 +45,14 @@ public sealed class UiBuilder : IDisposable /// The plugin namespace. internal UiBuilder(string namespaceName) { - try - { - this.stopwatch = new Stopwatch(); - this.hitchDetector = new HitchDetector($"UiBuilder({namespaceName})", this.configuration.UiBuilderHitch); - this.namespaceName = namespaceName; + this.stopwatch = new Stopwatch(); + this.hitchDetector = new HitchDetector($"UiBuilder({namespaceName})", this.configuration.UiBuilderHitch); + this.namespaceName = namespaceName; - 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; - } + this.interfaceManager.Draw += this.OnDraw; + this.interfaceManager.BuildFonts += this.OnBuildFonts; + this.interfaceManager.AfterBuildFonts += this.OnAfterBuildFonts; + this.interfaceManager.ResizeBuffers += this.OnResizeBuffers; } /// @@ -101,19 +80,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. + /// pointers inside this handler.
+ /// PLEASE remove this handler inside Dispose, or when you no longer need your fonts! ///
- [Obsolete($"Use {nameof(this.FontAtlas)} instead.", false)] - public event Action? BuildFonts; + 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. + /// pointers inside this handler.
+ /// PLEASE remove this handler inside Dispose, or when you no longer need your fonts! ///
- [Obsolete($"Use {nameof(this.FontAtlas)} instead.", false)] - public event Action? AfterBuildFonts; + public event Action AfterBuildFonts; /// /// Gets or sets an action that is called when plugin UI or interface modifications are supposed to be shown. @@ -128,57 +107,18 @@ public sealed class UiBuilder : IDisposable public event Action HideUi; /// - /// Gets the default Dalamud font size in points. + /// Gets the default Dalamud font based on Noto Sans CJK Medium in 17pt - supporting all game languages and icons. /// - 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.
- /// Accessing this static property outside of is dangerous and not supported. + /// Gets the default Dalamud icon font based on FontAwesome 5 Free solid in 17pt. ///
- /// - /// 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.
- /// Accessing this static property outside of is dangerous and not supported. + /// Gets the default Dalamud monospaced font based on Inconsolata Regular in 16pt. ///
- /// - /// 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; /// @@ -250,11 +190,6 @@ 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. /// @@ -384,7 +319,7 @@ public sealed class UiBuilder : IDisposable if (runInFrameworkThread) { return this.InterfaceManagerWithSceneAsync - .ContinueWith(_ => this.framework.RunOnFrameworkThread(func)) + .ContinueWith(_ => Service.Get().RunOnFrameworkThread(func)) .Unwrap(); } else @@ -406,7 +341,7 @@ public sealed class UiBuilder : IDisposable if (runInFrameworkThread) { return this.InterfaceManagerWithSceneAsync - .ContinueWith(_ => this.framework.RunOnFrameworkThread(func)) + .ContinueWith(_ => Service.Get().RunOnFrameworkThread(func)) .Unwrap(); } else @@ -422,49 +357,19 @@ public sealed class UiBuilder : IDisposable ///
/// Font to get. /// Handle to the game font which may or may not be available for use yet. - [Obsolete($"Use {nameof(this.FontAtlas)}.{nameof(IFontAtlas.NewGameFontHandle)} instead.", false)] - public GameFontHandle GetGameFontHandle(GameFontStyle style) => new( - (IFontHandle.IInternal)this.FontAtlas.NewGameFontHandle(style), - Service.Get()); + public GameFontHandle GetGameFontHandle(GameFontStyle style) => this.gameFontManager.NewFontRef(style); /// /// Call this to queue a rebuild of the font atlas.
- /// This will invoke any and handlers and ensure that any - /// loaded fonts are ready to be used on the next UI frame. + /// This will invoke any 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); - if (this.AfterBuildFonts is null && this.BuildFonts is null) - this.FontAtlas.BuildFontsAsync(); - else - this.FontAtlas.BuildFontsOnNextFrame(); + this.interfaceManager.RebuildFonts(); } - /// - /// 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. /// @@ -487,7 +392,12 @@ public sealed class UiBuilder : IDisposable /// /// Unregister the UiBuilder. Do not call this in plugin code. /// - void IDisposable.Dispose() => this.scopedFinalizer.Dispose(); + void IDisposable.Dispose() + { + this.interfaceManager.Draw -= this.OnDraw; + this.interfaceManager.BuildFonts -= this.OnBuildFonts; + this.interfaceManager.ResizeBuffers -= this.OnResizeBuffers; + } /// /// Open the registered configuration UI, if it exists. @@ -553,12 +463,8 @@ public sealed class UiBuilder : IDisposable this.ShowUi?.InvokeSafely(); } - // 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)) - { + if (!this.interfaceManager.FontsReady) return; - } ImGui.PushID(this.namespaceName); if (DoStats) @@ -620,28 +526,14 @@ public sealed class UiBuilder : IDisposable this.hitchDetector.Stop(); } - private unsafe void PrivateAtlasOnBuildStepChange(IFontAtlasBuildToolkit e) + private void OnBuildFonts() { - if (e.IsAsyncBuildOperation) - return; + this.BuildFonts?.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 OnAfterBuildFonts() + { + this.AfterBuildFonts?.InvokeSafely(); } private void OnResizeBuffers() diff --git a/Dalamud/Interface/Utility/ImGuiHelpers.cs b/Dalamud/Interface/Utility/ImGuiHelpers.cs index 444463d41..ad151ec4e 100644 --- a/Dalamud/Interface/Utility/ImGuiHelpers.cs +++ b/Dalamud/Interface/Utility/ImGuiHelpers.cs @@ -1,15 +1,10 @@ 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; @@ -36,7 +31,8 @@ public static class ImGuiHelpers /// This does not necessarily mean you can call drawing functions. /// public static unsafe bool IsImGuiInitialized => - ImGui.GetCurrentContext() != nint.Zero && ImGui.GetIO().NativePtr is not null; + ImGui.GetCurrentContext() is not (nint)0 // KW: IDEs get mad without the cast, despite being unnecessary + && ImGui.GetIO().NativePtr is not null; /// /// Gets the global Dalamud scale; even available before drawing is ready.
@@ -202,7 +198,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); @@ -314,7 +310,6 @@ public static class ImGuiHelpers glyph->U1, glyph->V1, glyph->AdvanceX * scale); - target.Mark4KPageUsedAfterGlyphAdd((ushort)glyph->Codepoint); changed = true; } else if (!missingOnly) @@ -348,18 +343,25 @@ public static class ImGuiHelpers } if (changed && rebuildLookupTable) - { - // 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; + target.BuildLookupTableNonstandard(); + } - target.BuildLookupTable(); - } + /// + /// 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(); } /// @@ -405,103 +407,6 @@ 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. /// @@ -510,7 +415,7 @@ public static class ImGuiHelpers public static unsafe bool IsNull(this ImFontPtr ptr) => ptr.NativePtr == null; /// - /// Determines whether is empty. + /// Determines whether is not null and loaded. /// /// The pointer. /// Whether it is empty. @@ -522,27 +427,6 @@ 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. @@ -564,89 +448,6 @@ 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. ///