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