diff --git a/Dalamud/Configuration/Internal/DalamudConfiguration.cs b/Dalamud/Configuration/Internal/DalamudConfiguration.cs index fe2fc97a6..7dd5c2ac8 100644 --- a/Dalamud/Configuration/Internal/DalamudConfiguration.cs +++ b/Dalamud/Configuration/Internal/DalamudConfiguration.cs @@ -141,7 +141,12 @@ namespace Dalamud.Configuration.Internal /// * ...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. /// - public float FontGamma { get; set; } = 1.0f; + public float FontGamma { get; set; } = 1.4f; + + /// + /// Gets or sets a value indicating whether to allow big font atlas. + /// + public bool AllowBigFontAtlas { get; set; } = false; /// /// Gets or sets a value indicating whether or not plugin UI should be hidden. diff --git a/Dalamud/Interface/GameFonts/GameFontManager.cs b/Dalamud/Interface/GameFonts/GameFontManager.cs index ba9253a9b..adbe6dfa4 100644 --- a/Dalamud/Interface/GameFonts/GameFontManager.cs +++ b/Dalamud/Interface/GameFonts/GameFontManager.cs @@ -117,18 +117,21 @@ namespace Dalamud.Interface.GameFonts /// Target font. /// Whether to copy missing glyphs only. /// Whether to call target.BuildLookupTable(). - public static void CopyGlyphsAcrossFonts(ImFontPtr? source, ImFontPtr? target, bool missingOnly, bool rebuildLookupTable) + /// Low codepoint range to copy. + /// High codepoing range to copy. + public static void CopyGlyphsAcrossFonts(ImFontPtr? source, ImFontPtr? target, bool missingOnly, bool rebuildLookupTable, int rangeLow = 32, int rangeHigh = 0xFFFE) { if (!source.HasValue || !target.HasValue) return; + var scale = target.Value!.FontSize / source.Value!.FontSize; unsafe { var glyphs = (ImFontGlyphReal*)source.Value!.Glyphs.Data; for (int j = 0, j_ = source.Value!.Glyphs.Size; j < j_; j++) { var glyph = &glyphs[j]; - if (glyph->Codepoint < 32 || glyph->Codepoint >= 0xFFFF) + if (glyph->Codepoint < rangeLow || glyph->Codepoint > rangeHigh) continue; var prevGlyphPtr = (ImFontGlyphReal*)target.Value!.FindGlyphNoFallback((ushort)glyph->Codepoint).NativePtr; @@ -137,27 +140,27 @@ namespace Dalamud.Interface.GameFonts target.Value!.AddGlyph( target.Value!.ConfigData, (ushort)glyph->Codepoint, - glyph->X0, - glyph->Y0, - glyph->X0 + ((glyph->X1 - glyph->X0) * target.Value!.FontSize / source.Value!.FontSize), - glyph->Y0 + ((glyph->Y1 - glyph->Y0) * target.Value!.FontSize / source.Value!.FontSize), + glyph->X0 * scale, + ((glyph->Y0 - source.Value!.Ascent) * scale) + target.Value!.Ascent, + glyph->X1 * scale, + ((glyph->Y1 - source.Value!.Ascent) * scale) + target.Value!.Ascent, glyph->U0, glyph->V0, glyph->U1, glyph->V1, - glyph->AdvanceX * target.Value!.FontSize / source.Value!.FontSize); + glyph->AdvanceX * scale); } else if (!missingOnly) { - prevGlyphPtr->X0 = glyph->X0; - prevGlyphPtr->Y0 = glyph->Y0; - prevGlyphPtr->X1 = glyph->X0 + ((glyph->X1 - glyph->X0) * target.Value!.FontSize / source.Value!.FontSize); - prevGlyphPtr->Y1 = glyph->Y0 + ((glyph->Y1 - glyph->Y0) * target.Value!.FontSize / source.Value!.FontSize); + prevGlyphPtr->X0 = glyph->X0 * scale; + prevGlyphPtr->Y0 = ((glyph->Y0 - source.Value!.Ascent) * scale) + target.Value!.Ascent; + prevGlyphPtr->X1 = glyph->X1 * scale; + prevGlyphPtr->Y1 = ((glyph->Y1 - source.Value!.Ascent) * scale) + target.Value!.Ascent; prevGlyphPtr->U0 = glyph->U0; prevGlyphPtr->V0 = glyph->V0; prevGlyphPtr->U1 = glyph->U1; prevGlyphPtr->V1 = glyph->V1; - prevGlyphPtr->AdvanceX = glyph->AdvanceX * target.Value!.FontSize / source.Value!.FontSize; + prevGlyphPtr->AdvanceX = glyph->AdvanceX * scale; } } } @@ -166,6 +169,39 @@ namespace Dalamud.Interface.GameFonts target.Value!.BuildLookupTable(); } + /// + /// 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) + { + unsafe + { + var font = fontPtr.NativePtr; + for (int i = 0, i_ = font->IndexAdvanceX.Size; i < i_; ++i) + ((float*)font->IndexAdvanceX.Data)[i] /= fontScale; + font->FallbackAdvanceX /= fontScale; + font->FontSize /= fontScale; + font->Ascent /= fontScale; + font->Descent /= 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; + } + } + + if (rebuildLookupTable) + fontPtr.BuildLookupTable(); + } + /// public void Dispose() { @@ -248,39 +284,48 @@ namespace Dalamud.Interface.GameFonts /// public void BuildFonts() { - var io = ImGui.GetIO(); - io.Fonts.TexDesiredWidth = 4096; - - this.glyphRectIds.Clear(); - this.fonts.Clear(); - - foreach (var style in this.fontUseCounter.Keys) + unsafe { - var rectIds = this.glyphRectIds[style] = new(); + ImFontConfigPtr fontConfig = ImGuiNative.ImFontConfig_ImFontConfig(); + fontConfig.OversampleH = 1; + fontConfig.OversampleV = 1; + fontConfig.PixelSnapH = true; - var fdt = this.fdts[(int)style.FamilyAndSize]; - if (fdt == null) - continue; + var io = ImGui.GetIO(); - var font = io.Fonts.AddFontDefault(); - this.fonts[style] = font; - foreach (var glyph in fdt.Glyphs) + this.glyphRectIds.Clear(); + this.fonts.Clear(); + + foreach (var style in this.fontUseCounter.Keys) { - var c = glyph.Char; - if (c < 32 || c >= 0xFFFF) + var rectIds = this.glyphRectIds[style] = new(); + + var fdt = this.fdts[(int)style.FamilyAndSize]; + if (fdt == null) continue; - var widthAdjustment = style.CalculateWidthAdjustment(fdt, glyph); - rectIds[c] = Tuple.Create( - io.Fonts.AddCustomRectFontGlyph( - font, - c, - glyph.BoundingWidth + widthAdjustment + 1, - glyph.BoundingHeight + 1, - glyph.AdvanceWidth, - new Vector2(0, glyph.CurrentOffsetY)), - glyph); + var font = io.Fonts.AddFontDefault(fontConfig); + this.fonts[style] = font; + foreach (var glyph in fdt.Glyphs) + { + var c = glyph.Char; + if (c < 32 || c >= 0xFFFF) + continue; + + var widthAdjustment = style.CalculateWidthAdjustment(fdt, glyph); + rectIds[c] = Tuple.Create( + io.Fonts.AddCustomRectFontGlyph( + font, + c, + glyph.BoundingWidth + widthAdjustment + 1, + glyph.BoundingHeight + 1, + glyph.AdvanceWidth, + new Vector2(0, glyph.CurrentOffsetY)), + glyph); + } } + + fontConfig.Destroy(); } } @@ -298,7 +343,7 @@ namespace Dalamud.Interface.GameFonts { var fdt = this.fdts[(int)style.FamilyAndSize]; var fontPtr = font.NativePtr; - fontPtr->ConfigData->SizePixels = fontPtr->FontSize = fdt.FontHeader.LineHeight; + fontPtr->ConfigData->SizePixels = fontPtr->FontSize = fdt.FontHeader.Size * 4 / 3; fontPtr->Ascent = fdt.FontHeader.Ascent; fontPtr->Descent = fdt.FontHeader.Descent; fontPtr->EllipsisChar = '…'; @@ -399,6 +444,9 @@ namespace Dalamud.Interface.GameFonts { lock (this.syncRoot) { + if (!this.fontUseCounter.ContainsKey(style)) + return; + if ((this.fontUseCounter[style] -= 1) == 0) this.fontUseCounter.Remove(style); } diff --git a/Dalamud/Interface/Internal/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs index 0c6d99efa..c19a32601 100644 --- a/Dalamud/Interface/Internal/InterfaceManager.cs +++ b/Dalamud/Interface/Internal/InterfaceManager.cs @@ -47,8 +47,16 @@ namespace Dalamud.Interface.Internal /// internal class InterfaceManager : IDisposable { + private const float DefaultFontSizePt = 12.0f; + private 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 string rtssPath; + private readonly HashSet glyphRequests = new(); + private readonly List axisFontHandles = new(); + private readonly Hook presentHook; private readonly Hook resizeBuffersHook; private readonly Hook setCursorHook; @@ -57,8 +65,6 @@ namespace Dalamud.Interface.Internal private readonly SwapChainVtableResolver address; private RawDX11Scene? scene; - private GameFontHandle? axisFontHandle; - // can't access imgui IO before first present call private bool lastWantCapture = false; private bool isRebuildingFonts = false; @@ -189,6 +195,16 @@ namespace Dalamud.Interface.Internal /// public bool IsReady => this.scene != null; + /// + /// 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. /// @@ -199,6 +215,16 @@ namespace Dalamud.Interface.Internal /// public float FontGamma => Math.Max(0.1f, this.FontGammaOverride.GetValueOrDefault(Service.Get().FontGamma)); + /// + /// Gets or sets a value indicating whether to override configuration for AllowBigFontAtlas. + /// + public bool? AllowBigFontAtlasOverride { get; set; } = null; + + /// + /// Gets a value indicating whether to allow big font atlas. + /// + public bool AllowBigFontAtlas => this.AllowBigFontAtlasOverride ?? Service.Get().AllowBigFontAtlas; + /// /// Enable this module. /// @@ -322,8 +348,6 @@ namespace Dalamud.Interface.Internal if (!this.isRebuildingFonts) { Log.Verbose("[FONT] RebuildFonts() trigger"); - this.SetAxisFonts(); - this.isRebuildingFonts = true; this.scene.OnNewRenderFrame += this.RebuildFontsInternal; } @@ -337,31 +361,75 @@ namespace Dalamud.Interface.Internal 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); + } + private static void ShowFontError(string path) { Util.Fatal($"One or more files required by XIVLauncher were not found.\nPlease restart and report this error if it occurs again.\n\n{path}", "Error"); } - private void SetAxisFonts() - { - var configuration = Service.Get(); - if (configuration.UseAxisFontsFromGame) - { - var currentFamilyAndSize = GameFontStyle.GetRecommendedFamilyAndSize(GameFontFamily.Axis, this.axisFontHandle?.Style.Size ?? 0f); - var expectedFamilyAndSize = GameFontStyle.GetRecommendedFamilyAndSize(GameFontFamily.Axis, 12 * ImGui.GetIO().FontGlobalScale); - if (currentFamilyAndSize == expectedFamilyAndSize) - return; - - this.axisFontHandle?.Dispose(); - this.axisFontHandle = Service.Get().NewFontRef(new(expectedFamilyAndSize)); - } - else - { - this.axisFontHandle?.Dispose(); - this.axisFontHandle = null; - } - } - /* * NOTE(goat): When hooking ReShade DXGISwapChain::runtime_present, this is missing the syncInterval arg. * Seems to work fine regardless, I guess, so whatever. @@ -423,8 +491,6 @@ namespace Dalamud.Interface.Internal this.scene.OnBuildUI += this.Display; this.scene.OnNewInputFrame += this.OnNewInputFrame; - this.SetAxisFonts(); - this.SetupFonts(); StyleModel.TransferOldModels(); @@ -530,105 +596,276 @@ namespace Dalamud.Interface.Internal private unsafe void SetupFonts() { + var gameFontManager = Service.Get(); var dalamud = Service.Get(); - var ioFonts = ImGui.GetIO().Fonts; + var io = ImGui.GetIO(); + var ioFonts = io.Fonts; + var fontLoadScale = this.AllowBigFontAtlas ? io.FontGlobalScale : 1; var fontGamma = this.FontGamma; + List fontsToUnscale = new(); + List fontsToOverwriteFromAxis = new(); + List fontsToReassignSizes = new(); this.fontBuildSignal.Reset(); - ioFonts.Clear(); + ioFonts.TexDesiredWidth = this.AllowBigFontAtlas ? 4096 : 2048; - ImFontConfigPtr fontConfig = ImGuiNative.ImFontConfig_ImFontConfig(); - fontConfig.PixelSnapH = true; + Log.Verbose("[FONT] SetupFonts - 1"); - var fontPathJp = Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "NotoSansCJKjp-Medium.otf"); - - if (!File.Exists(fontPathJp)) - ShowFontError(fontPathJp); - - var japaneseRangeHandle = GCHandle.Alloc(GlyphRangesJapanese.GlyphRanges, GCHandleType.Pinned); - - DefaultFont = ioFonts.AddFontFromFileTTF(fontPathJp, 17.0f, null, japaneseRangeHandle.AddrOfPinnedObject()); - - var fontPathGame = Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "gamesym.ttf"); - - if (!File.Exists(fontPathGame)) - ShowFontError(fontPathGame); - - var gameRangeHandle = GCHandle.Alloc( - new ushort[] - { - 0xE020, - 0xE0DB, - 0, - }, - GCHandleType.Pinned); - - fontConfig.MergeMode = false; - ioFonts.AddFontFromFileTTF(fontPathGame, 17.0f, fontConfig, gameRangeHandle.AddrOfPinnedObject()); - fontConfig.MergeMode = true; - - var fontPathIcon = Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "FontAwesome5FreeSolid.otf"); - - if (!File.Exists(fontPathIcon)) - ShowFontError(fontPathIcon); - - var iconRangeHandle = GCHandle.Alloc( - new ushort[] - { - 0xE000, - 0xF8FF, - 0, - }, - GCHandleType.Pinned); - IconFont = ioFonts.AddFontFromFileTTF(fontPathIcon, 17.0f, null, iconRangeHandle.AddrOfPinnedObject()); - - var fontPathMono = Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "Inconsolata-Regular.ttf"); - - if (!File.Exists(fontPathMono)) - ShowFontError(fontPathMono); - - MonoFont = ioFonts.AddFontFromFileTTF(fontPathMono, 16.0f); - - var gameFontManager = Service.Get(); - gameFontManager.BuildFonts(); - - Log.Verbose("[FONT] Invoke OnBuildFonts"); - this.BuildFonts?.Invoke(); - Log.Verbose("[FONT] OnBuildFonts OK!"); - - for (var i = 0; i < ImGui.GetIO().Fonts.Fonts.Size; i++) + foreach (var v in this.axisFontHandles) { - Log.Verbose("{0} - {1}", i, ImGui.GetIO().Fonts.Fonts[i].GetDebugName()); + if (v != null) + v.Dispose(); } - ioFonts.Build(); + this.axisFontHandles.Clear(); - if (Math.Abs(fontGamma - 1.0f) >= 0.001) + Log.Verbose("[FONT] SetupFonts - 2"); + + ImFontConfigPtr fontConfig = null; + List garbageList = new(); + + try { - // 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) - ioFonts.GetTexDataAsRGBA32(out byte* texPixels, out var texWidth, out var texHeight); - for (int i = 3, i_ = texWidth * texHeight * 4; i < i_; i += 4) - texPixels[i] = (byte)(Math.Pow(texPixels[i] / 255.0f, 1.0f / fontGamma) * 255.0f); + var dummyRangeHandle = GCHandle.Alloc(new ushort[] { '0', '0', 0 }, GCHandleType.Pinned); + garbageList.Add(dummyRangeHandle); + + fontConfig = ImGuiNative.ImFontConfig_ImFontConfig(); + fontConfig.OversampleH = 1; + fontConfig.OversampleV = 1; + fontConfig.PixelSnapH = true; + + var fontPathJp = Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "NotoSansCJKjp-Medium.otf"); + if (!File.Exists(fontPathJp)) + ShowFontError(fontPathJp); + + // Default font + Log.Verbose("[FONT] SetupFonts - Default font"); + this.axisFontHandles.Add(gameFontManager.NewFontRef(this.AllowBigFontAtlas ? new(GameFontFamily.Axis, DefaultFontSizePt * fontLoadScale) : new(GameFontFamilyAndSize.Axis12))); + if (this.UseAxis) + { + fontConfig.GlyphRanges = dummyRangeHandle.AddrOfPinnedObject(); + fontConfig.SizePixels = DefaultFontSizePx * fontLoadScale; + DefaultFont = ioFonts.AddFontDefault(fontConfig); + fontsToUnscale.Add(DefaultFont); + fontsToOverwriteFromAxis.Add(true); + fontsToReassignSizes.Add(null); + } + else + { + var japaneseRangeHandle = GCHandle.Alloc(GlyphRangesJapanese.GlyphRanges, GCHandleType.Pinned); + garbageList.Add(japaneseRangeHandle); + + fontConfig.GlyphRanges = japaneseRangeHandle.AddrOfPinnedObject(); + DefaultFont = ioFonts.AddFontFromFileTTF(fontPathJp, (DefaultFontSizePx + 1) * fontLoadScale, fontConfig); + fontsToUnscale.Add(DefaultFont); + fontsToOverwriteFromAxis.Add(false); + fontsToReassignSizes.Add(null); + } + + // FontAwesome icon font + Log.Verbose("[FONT] SetupFonts - FontAwesome icon font"); + { + var fontPathIcon = Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "FontAwesome5FreeSolid.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(); + IconFont = ioFonts.AddFontFromFileTTF(fontPathIcon, DefaultFontSizePx * fontLoadScale, fontConfig); + fontsToUnscale.Add(IconFont); + this.axisFontHandles.Add(null); + fontsToOverwriteFromAxis.Add(false); + fontsToReassignSizes.Add(null); + } + + // 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; + MonoFont = ioFonts.AddFontFromFileTTF(fontPathMono, DefaultFontSizePx * fontLoadScale, fontConfig); + fontsToUnscale.Add(MonoFont); + this.axisFontHandles.Add(null); + fontsToOverwriteFromAxis.Add(false); + fontsToReassignSizes.Add(null); + } + + // 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> codepointRanges = new(); + codepointRanges.Add(Tuple.Create(Fallback1Codepoint, Fallback1Codepoint)); + codepointRanges.Add(Tuple.Create(Fallback2Codepoint, Fallback2Codepoint)); + + // ImGui default ellipsis characters + codepointRanges.Add(Tuple.Create(0x2026, 0x2026)); + codepointRanges.Add(Tuple.Create(0x0085, 0x0085)); + + foreach (var request in requests) + { + foreach (var range in request.CodepointRanges) + codepointRanges.Add(range); + } + + codepointRanges.Sort((x, y) => (x.Item1 == y.Item1 ? (x.Item2 < y.Item2 ? -1 : (x.Item2 == y.Item2 ? 0 : 1)) : (x.Item1 < y.Item1 ? -1 : 1))); + + 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); + + ImFontPtr sizedFont; + this.axisFontHandles.Add(gameFontManager.NewFontRef(this.AllowBigFontAtlas ? new(GameFontFamily.Axis, fontSize * 3 / 4 * fontLoadScale) : new(GameFontFamilyAndSize.Axis12))); + if (this.UseAxis) + { + fontConfig.GlyphRanges = dummyRangeHandle.AddrOfPinnedObject(); + fontConfig.SizePixels = (this.AllowBigFontAtlas ? fontSize : DefaultFontSizePx) * fontLoadScale; + sizedFont = ioFonts.AddFontDefault(fontConfig); + fontsToUnscale.Add(sizedFont); + fontsToOverwriteFromAxis.Add(true); + fontsToReassignSizes.Add(this.AllowBigFontAtlas ? null : fontSize); + } + else + { + var rangeHandle = GCHandle.Alloc(flattenedRanges.ToArray(), GCHandleType.Pinned); + garbageList.Add(rangeHandle); + sizedFont = ioFonts.AddFontFromFileTTF(fontPathJp, (this.AllowBigFontAtlas ? fontSize : DefaultFontSizePx + 1) * fontLoadScale, fontConfig, rangeHandle.AddrOfPinnedObject()); + fontsToUnscale.Add(sizedFont); + fontsToOverwriteFromAxis.Add(false); + fontsToReassignSizes.Add(this.AllowBigFontAtlas ? null : fontSize); + } + + foreach (var request in requests) + request.FontInternal = sizedFont; + } + } + + gameFontManager.BuildFonts(); + + Log.Verbose("[FONT] Invoke OnBuildFonts"); + this.BuildFonts?.Invoke(); + Log.Verbose("[FONT] OnBuildFonts OK!"); + + for (var i = 0; i < ImGui.GetIO().Fonts.Fonts.Size; i++) + { + Log.Verbose("{0} - {1}", i, ImGui.GetIO().Fonts.Fonts[i].GetDebugName()); + } + + ioFonts.Build(); + + if (Math.Abs(fontGamma - 1.0f) >= 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) + ioFonts.GetTexDataAsRGBA32(out byte* texPixels, out var texWidth, out var texHeight); + for (int i = 3, i_ = texWidth * texHeight * 4; i < i_; i += 4) + texPixels[i] = (byte)(Math.Pow(texPixels[i] / 255.0f, 1.0f / fontGamma) * 255.0f); + } + + gameFontManager.AfterBuildFonts(); + + for (var i = 0; i < fontsToUnscale.Count; i++) + { + var font = fontsToUnscale[i]; + var fontPtr = font.NativePtr; + var correspondingAxis = this.axisFontHandles[i]; + var overwrite = fontsToOverwriteFromAxis[i]; + var overwriteSize = fontsToReassignSizes[i]; + + GameFontManager.UnscaleFont(font, fontLoadScale, false); + + if (correspondingAxis == null) + continue; + + var scale = 1f; + + if (overwrite) + { + var srcPtr = correspondingAxis.ImFont.NativePtr; + scale = fontPtr->ConfigData->SizePixels / srcPtr->ConfigData->SizePixels / fontLoadScale; + fontPtr->FontSize = srcPtr->FontSize * scale; + fontPtr->Ascent = srcPtr->Ascent * scale; + fontPtr->Descent = srcPtr->Descent * scale; + fontPtr->FallbackChar = srcPtr->FallbackChar; + fontPtr->EllipsisChar = srcPtr->EllipsisChar; + GameFontManager.CopyGlyphsAcrossFonts(correspondingAxis.ImFont, font, false, false); + + scale = 1f; + } + + if (overwriteSize != null) + scale *= overwriteSize.Value / fontPtr->ConfigData->SizePixels; + + if (scale != 1f) + GameFontManager.UnscaleFont(font, 1 / scale, false); + + Log.Verbose("[FONT] Font {0}: result size {1}", i, fontPtr->FontSize); + + if (!this.UseAxis && fontPtr == DefaultFont.NativePtr) + { + fontPtr->FontSize -= 1; + GameFontManager.CopyGlyphsAcrossFonts(correspondingAxis.ImFont, font, true, false, 0xE020, 0xE0DB); + fontPtr->FontSize += 1; + } + else + { + GameFontManager.CopyGlyphsAcrossFonts(correspondingAxis.ImFont, font, true, false, 0xE020, 0xE0DB); + } + } + + // Fill missing glyphs in MonoFont from DefaultFont + GameFontManager.CopyGlyphsAcrossFonts(DefaultFont, MonoFont, true, false); + + foreach (var font in fontsToUnscale) + { + font.FallbackChar = Fallback1Codepoint; + font.BuildLookupTable(); + } + + Log.Verbose("[FONT] Invoke OnAfterBuildFonts"); + this.AfterBuildFonts?.Invoke(); + Log.Verbose("[FONT] OnAfterBuildFonts OK!"); + + Log.Verbose("[FONT] Fonts built!"); + + this.fontBuildSignal.Set(); + + this.FontsReady = true; } + finally + { + if (fontConfig.NativePtr != null) + fontConfig.Destroy(); - gameFontManager.AfterBuildFonts(); - GameFontManager.CopyGlyphsAcrossFonts(this.axisFontHandle?.ImFont, DefaultFont, false, true); - - Log.Verbose("[FONT] Invoke OnAfterBuildFonts"); - this.AfterBuildFonts?.Invoke(); - Log.Verbose("[FONT] OnAfterBuildFonts OK!"); - - Log.Verbose("[FONT] Fonts built!"); - - this.fontBuildSignal.Set(); - - fontConfig.Destroy(); - japaneseRangeHandle.Free(); - gameRangeHandle.Free(); - iconRangeHandle.Free(); - - this.FontsReady = true; + foreach (var garbage in garbageList) + garbage.Free(); + } } private void Disable() @@ -765,5 +1002,65 @@ namespace Dalamud.Interface.Internal Service.Get().Draw(); } + + /// + /// 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); + } + } } } diff --git a/Dalamud/Interface/Internal/Windows/SettingsWindow.cs b/Dalamud/Interface/Internal/Windows/SettingsWindow.cs index a3adb3780..e94d7890b 100644 --- a/Dalamud/Interface/Internal/Windows/SettingsWindow.cs +++ b/Dalamud/Interface/Internal/Windows/SettingsWindow.cs @@ -26,7 +26,7 @@ namespace Dalamud.Interface.Internal.Windows internal class SettingsWindow : Window { private const float MinScale = 0.3f; - private const float MaxScale = 2.0f; + private const float MaxScale = 3.0f; private readonly string[] languages; private readonly string[] locLanguages; @@ -39,6 +39,7 @@ namespace Dalamud.Interface.Internal.Windows private float globalUiScale; private bool doUseAxisFontsFromGame; + private bool doAllowBigFontAtlas; private float fontGamma; private bool doToggleUiHide; private bool doToggleUiHideDuringCutscenes; @@ -96,6 +97,7 @@ namespace Dalamud.Interface.Internal.Windows this.globalUiScale = configuration.GlobalUiScale; this.fontGamma = configuration.FontGamma; this.doUseAxisFontsFromGame = configuration.UseAxisFontsFromGame; + this.doAllowBigFontAtlas = configuration.AllowBigFontAtlas; this.doToggleUiHide = configuration.ToggleUiHide; this.doToggleUiHideDuringCutscenes = configuration.ToggleUiHideDuringCutscenes; this.doToggleUiHideDuringGpose = configuration.ToggleUiHideDuringGpose; @@ -188,6 +190,8 @@ namespace Dalamud.Interface.Internal.Windows ImGui.GetIO().FontGlobalScale = configuration.GlobalUiScale; interfaceManager.FontGammaOverride = null; + interfaceManager.AllowBigFontAtlasOverride = null; + interfaceManager.UseAxisOverride = null; this.thirdRepoList = configuration.ThirdRepoList.Select(x => x.Clone()).ToList(); this.devPluginLocations = configuration.DevPluginLoadLocations.Select(x => x.Clone()).ToList(); @@ -325,9 +329,22 @@ namespace Dalamud.Interface.Internal.Windows ImGui.TextColored(ImGuiColors.DalamudGrey, Loc.Localize("DalamudSettingToggleUiHideOptOutNote", "Plugins may independently opt out of the settings below.")); - ImGui.Checkbox(Loc.Localize("DalamudSettingToggleAxisFonts", "Use AXIS fonts as default Dalamud font"), ref this.doUseAxisFontsFromGame); + if (ImGui.Checkbox(Loc.Localize("DalamudSettingToggleAxisFonts", "Use AXIS fonts as default Dalamud font"), ref this.doUseAxisFontsFromGame)) + { + interfaceManager.UseAxisOverride = this.doUseAxisFontsFromGame; + interfaceManager.RebuildFonts(); + } + ImGui.TextColored(ImGuiColors.DalamudGrey, Loc.Localize("DalamudSettingToggleUiAxisFontsHint", "Use AXIS fonts (the game's main UI fonts) as default Dalamud font.")); + if (ImGui.Checkbox(Loc.Localize("DalamudSettingAllowBigFontAtlas", "Allow big font atlas"), ref this.doAllowBigFontAtlas)) + { + interfaceManager.AllowBigFontAtlasOverride = this.doAllowBigFontAtlas; + interfaceManager.RebuildFonts(); + } + + ImGui.TextColored(ImGuiColors.DalamudGrey, string.Format(Loc.Localize("DalamudSettingAllowBigFontAtlas", "Displays text crisply, but may crash if your GPU does not support it.\nCurrent size: {0}px * {1}px"), ImGui.GetIO().Fonts.TexWidth, ImGui.GetIO().Fonts.TexHeight)); + ImGui.Checkbox(Loc.Localize("DalamudSettingToggleUiHide", "Hide plugin UI when the game UI is toggled off"), ref this.doToggleUiHide); ImGui.TextColored(ImGuiColors.DalamudGrey, Loc.Localize("DalamudSettingToggleUiHideHint", "Hide any open windows by plugins when toggling the game overlay.")); @@ -856,6 +873,7 @@ namespace Dalamud.Interface.Internal.Windows configuration.Fools22Newer = this.doFools22; configuration.UseAxisFontsFromGame = this.doUseAxisFontsFromGame; + configuration.AllowBigFontAtlas = this.doAllowBigFontAtlas; configuration.FontGamma = this.fontGamma; // This is applied every frame in InterfaceManager::CheckViewportState() diff --git a/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs b/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs index b4c089fbe..40bb29672 100644 --- a/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs +++ b/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; using System.Numerics; using Dalamud.Configuration.Internal; @@ -20,17 +21,18 @@ namespace Dalamud.Interface.Internal.Windows /// internal class TitleScreenMenuWindow : Window, IDisposable { - private const float TargetFontSize = 16.2f; + private const float TargetFontSizePt = 18f; + private const float TargetFontSizePx = TargetFontSizePt * 4 / 3; + private readonly TextureWrap shadeTexture; private readonly Dictionary shadeEasings = new(); private readonly Dictionary moveEasings = new(); private readonly Dictionary logoEasings = new(); + private readonly Dictionary specialGlyphRequests = new(); private InOutCubic? fadeOutEasing; - private GameFontHandle? axisFontHandle; - private State state = State.Hide; /// @@ -71,19 +73,14 @@ namespace Dalamud.Interface.Internal.Windows /// public override void PreDraw() { - this.SetAxisFonts(); ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, new Vector2(0, 0)); ImGui.PushStyleVar(ImGuiStyleVar.WindowPadding, new Vector2(0, 0)); - if (this.axisFontHandle?.Available ?? false) - ImGui.PushFont(this.axisFontHandle.ImFont); base.PreDraw(); } /// public override void PostDraw() { - if (this.axisFontHandle?.Available ?? false) - ImGui.PopFont(); ImGui.PopStyleVar(2); base.PostDraw(); } @@ -99,7 +96,7 @@ namespace Dalamud.Interface.Internal.Windows /// public override void Draw() { - ImGui.SetWindowFontScale(TargetFontSize / ImGui.GetFont().FontSize * 4 / 3); + var scale = ImGui.GetIO().FontGlobalScale; var tsm = Service.Get(); @@ -129,7 +126,7 @@ namespace Dalamud.Interface.Internal.Windows moveEasing.Update(); - var finalPos = (i + 1) * this.shadeTexture.Height; + var finalPos = (i + 1) * this.shadeTexture.Height * scale; var pos = moveEasing.Value * finalPos; // FIXME(goat): Sometimes, easings can overshoot and bring things out of alignment. @@ -180,7 +177,7 @@ namespace Dalamud.Interface.Internal.Windows { var entry = tsm.Entries[i]; - var finalPos = (i + 1) * this.shadeTexture.Height; + var finalPos = (i + 1) * this.shadeTexture.Height * scale; this.DrawEntry(entry, i != 0, true, i == 0, false); @@ -222,26 +219,36 @@ namespace Dalamud.Interface.Internal.Windows break; } } - } - private void SetAxisFonts() - { - var configuration = Service.Get(); - if (configuration.UseAxisFontsFromGame) + var srcText = tsm.Entries.Select(e => e.Name).ToHashSet(); + var keys = this.specialGlyphRequests.Keys.ToHashSet(); + keys.RemoveWhere(x => srcText.Contains(x)); + foreach (var key in keys) { - if (this.axisFontHandle == null) - this.axisFontHandle = Service.Get().NewFontRef(new(GameFontFamily.Axis, TargetFontSize)); - } - else - { - this.axisFontHandle?.Dispose(); - this.axisFontHandle = null; + this.specialGlyphRequests[key].Dispose(); + this.specialGlyphRequests.Remove(key); } } private bool DrawEntry( TitleScreenMenu.TitleScreenMenuEntry entry, bool inhibitFadeout, bool showText, bool isFirst, bool overrideAlpha) { + InterfaceManager.SpecialGlyphRequest fontHandle; + if (this.specialGlyphRequests.TryGetValue(entry.Name, out fontHandle) && fontHandle.Size != TargetFontSizePx) + { + fontHandle.Dispose(); + this.specialGlyphRequests.Remove(entry.Name); + fontHandle = null; + } + + if (fontHandle == null) + this.specialGlyphRequests[entry.Name] = fontHandle = Service.Get().NewFontSizeRef(TargetFontSizePx, entry.Name); + + ImGui.PushFont(fontHandle.Font); + ImGui.SetWindowFontScale(TargetFontSizePx / fontHandle.Size); + + var scale = ImGui.GetIO().FontGlobalScale; + if (!this.shadeEasings.TryGetValue(entry.Id, out var shadeEasing)) { shadeEasing = new InOutCubic(TimeSpan.FromMilliseconds(350)); @@ -251,7 +258,7 @@ namespace Dalamud.Interface.Internal.Windows var initialCursor = ImGui.GetCursorPos(); ImGui.PushStyleVar(ImGuiStyleVar.Alpha, (float)shadeEasing.Value); - ImGui.Image(this.shadeTexture.ImGuiHandle, new Vector2(this.shadeTexture.Width, this.shadeTexture.Height)); + ImGui.Image(this.shadeTexture.ImGuiHandle, new Vector2(this.shadeTexture.Width * scale, this.shadeTexture.Height * scale)); ImGui.PopStyleVar(); var isHover = ImGui.IsItemHovered(); @@ -305,7 +312,7 @@ namespace Dalamud.Interface.Internal.Windows ImGui.PushStyleVar(ImGuiStyleVar.Alpha, 1f); } - ImGui.Image(entry.Texture.ImGuiHandle, new Vector2(TitleScreenMenu.TextureSize)); + ImGui.Image(entry.Texture.ImGuiHandle, new Vector2(TitleScreenMenu.TextureSize * scale)); if (overrideAlpha || isFirst) { ImGui.PopStyleVar(); @@ -319,23 +326,36 @@ namespace Dalamud.Interface.Internal.Windows var textHeight = ImGui.GetTextLineHeightWithSpacing(); var cursor = ImGui.GetCursorPos(); - cursor.Y += (entry.Texture.Height / 2) - (textHeight / 2); - ImGui.SetCursorPos(cursor); + cursor.Y += (entry.Texture.Height * scale / 2) - (textHeight / 2); if (overrideAlpha) { ImGui.PushStyleVar(ImGuiStyleVar.Alpha, showText ? (float)logoEasing.Value : 0f); } + // Drop shadow + ImGui.PushStyleColor(ImGuiCol.Text, 0xFF000000); + for (int i = 0, i_ = (int)Math.Ceiling(1 * scale); i < i_; i++) + { + ImGui.SetCursorPos(new Vector2(cursor.X, cursor.Y + i)); + ImGui.Text(entry.Name); + } + + ImGui.PopStyleColor(); + + ImGui.SetCursorPos(cursor); ImGui.Text(entry.Name); + if (overrideAlpha) { ImGui.PopStyleVar(); } - initialCursor.Y += entry.Texture.Height; + initialCursor.Y += entry.Texture.Height * scale; ImGui.SetCursorPos(initialCursor); + ImGui.PopFont(); + return isHover; } diff --git a/lib/CoreCLR/CoreCLR.cpp b/lib/CoreCLR/CoreCLR.cpp index 6f4d337b8..265f1869e 100644 --- a/lib/CoreCLR/CoreCLR.cpp +++ b/lib/CoreCLR/CoreCLR.cpp @@ -37,12 +37,12 @@ int CoreCLR::load_hostfxr(const struct get_hostfxr_parameters* parameters) && m_hostfxr_close_fptr ? 0 : -1; } -bool CoreCLR::load_runtime(const std::wstring& runtime_config_path) +int CoreCLR::load_runtime(const std::wstring& runtime_config_path) { return CoreCLR::load_runtime(runtime_config_path, nullptr); } -bool CoreCLR::load_runtime(const std::wstring& runtime_config_path, const struct hostfxr_initialize_parameters* parameters) +int CoreCLR::load_runtime(const std::wstring& runtime_config_path, const struct hostfxr_initialize_parameters* parameters) { int result; diff --git a/lib/CoreCLR/CoreCLR.h b/lib/CoreCLR/CoreCLR.h index 235bf9923..71c62d90e 100644 --- a/lib/CoreCLR/CoreCLR.h +++ b/lib/CoreCLR/CoreCLR.h @@ -12,8 +12,8 @@ class CoreCLR { int load_hostfxr(); int load_hostfxr(const get_hostfxr_parameters* parameters); - bool load_runtime(const std::wstring& runtime_config_path); - bool load_runtime( + int load_runtime(const std::wstring& runtime_config_path); + int load_runtime( const std::wstring& runtime_config_path, const struct hostfxr_initialize_parameters* parameters); diff --git a/lib/CoreCLR/boot.cpp b/lib/CoreCLR/boot.cpp index 2fde7e221..63636e709 100644 --- a/lib/CoreCLR/boot.cpp +++ b/lib/CoreCLR/boot.cpp @@ -90,7 +90,7 @@ int InitializeClrAndGetEntryPoint( printf("Loading hostfxr... "); if ((result = g_clr->load_hostfxr(&init_parameters)) != 0) { - printf("\nError: Failed to load the `hostfxr` library (err=%d)\n", result); + printf("\nError: Failed to load the `hostfxr` library (err=0x%08x)\n", result); return result; } printf("Done!\n");