From d70b430e0dd19b934b74c39591cd3c504747b6f0 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Sun, 21 Jan 2024 03:10:41 +0900 Subject: [PATCH 1/9] Add IFontHandle.Lock and WaitAsync --- Dalamud/Interface/GameFonts/GameFontHandle.cs | 16 +- .../Widgets/GamePrebakedFontsTestWidget.cs | 53 ++++++ .../Interface/ManagedFontAtlas/IFontHandle.cs | 80 ++++++++- .../Internals/DelegateFontHandle.cs | 118 ++++++++++++-- .../FontAtlasFactory.Implementation.cs | 153 +++++++++++------- .../Internals/GamePrebakedFontHandle.cs | 117 +++++++++++++- .../Internals/IFontHandleManager.cs | 10 +- .../Internals/IFontHandleSubstance.cs | 5 + Dalamud/Utility/IRefCountable.cs | 77 +++++++++ 9 files changed, 543 insertions(+), 86 deletions(-) create mode 100644 Dalamud/Utility/IRefCountable.cs diff --git a/Dalamud/Interface/GameFonts/GameFontHandle.cs b/Dalamud/Interface/GameFonts/GameFontHandle.cs index d11414517..6591ce0fe 100644 --- a/Dalamud/Interface/GameFonts/GameFontHandle.cs +++ b/Dalamud/Interface/GameFonts/GameFontHandle.cs @@ -1,4 +1,5 @@ using System.Numerics; +using System.Threading.Tasks; using Dalamud.Interface.ManagedFontAtlas; using Dalamud.Interface.ManagedFontAtlas.Internals; @@ -28,6 +29,13 @@ public sealed class GameFontHandle : IFontHandle this.fontAtlasFactory = fontAtlasFactory; } + /// + public event Action ImFontChanged + { + add => this.fontHandle.ImFontChanged += value; + remove => this.fontHandle.ImFontChanged -= value; + } + /// public Exception? LoadException => this.fontHandle.LoadException; @@ -55,15 +63,21 @@ public sealed class GameFontHandle : IFontHandle /// public void Dispose() => this.fontHandle.Dispose(); + /// + public IFontHandle.ImFontLocked Lock() => this.fontHandle.Lock(); + /// /// Pushes the font. /// /// An that can be used to pop the font on dispose. public IDisposable Push() => this.fontHandle.Push(); - + /// IFontHandle.FontPopper IFontHandle.Push() => this.fontHandle.Push(); + /// + public Task WaitAsync() => this.fontHandle.WaitAsync(); + /// /// Creates a new .
///
diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/GamePrebakedFontsTestWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/GamePrebakedFontsTestWidget.cs index dba293e8b..b3b57343c 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/GamePrebakedFontsTestWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/GamePrebakedFontsTestWidget.cs @@ -1,7 +1,9 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; +using System.Numerics; using System.Text; +using System.Threading.Tasks; using Dalamud.Interface.GameFonts; using Dalamud.Interface.ManagedFontAtlas; @@ -11,6 +13,8 @@ using Dalamud.Utility; using ImGuiNET; +using Serilog; + namespace Dalamud.Interface.Internal.Windows.Data.Widgets; /// @@ -103,6 +107,10 @@ internal class GamePrebakedFontsTestWidget : IDataWindowWidget, IDisposable minCapacity: 1024); } + ImGui.SameLine(); + if (ImGui.Button("Test Lock")) + Task.Run(this.TestLock); + fixed (byte* labelPtr = "Test Input"u8) { if (ImGuiNative.igInputTextMultiline( @@ -210,4 +218,49 @@ internal class GamePrebakedFontsTestWidget : IDataWindowWidget, IDisposable this.privateAtlas?.Dispose(); this.privateAtlas = null; } + + private async void TestLock() + { + if (this.fontHandles is not { } fontHandlesCopy) + return; + + Log.Information($"{nameof(GamePrebakedFontsTestWidget)}: {nameof(this.TestLock)} waiting for build"); + + await using var garbage = new DisposeSafety.ScopedFinalizer(); + var fonts = new List(); + IFontHandle[] handles; + try + { + handles = fontHandlesCopy.Values.SelectMany(x => x).Select(x => x.Handle.Value).ToArray(); + foreach (var handle in handles) + { + await handle.WaitAsync(); + var locked = handle.Lock(); + garbage.Add(locked); + fonts.Add(locked); + } + } + catch (ObjectDisposedException) + { + Log.Information($"{nameof(GamePrebakedFontsTestWidget)}: {nameof(this.TestLock)} cancelled"); + return; + } + + Log.Information($"{nameof(GamePrebakedFontsTestWidget)}: {nameof(this.TestLock)} waiting in lock"); + await Task.Delay(5000); + + foreach (var (font, handle) in fonts.Zip(handles)) + TestSingle(font, handle); + + return; + + unsafe void TestSingle(ImFontPtr fontPtr, IFontHandle handle) + { + var dim = default(Vector2); + var test = "Test string"u8; + fixed (byte* pTest = test) + ImGuiNative.ImFont_CalcTextSizeA(&dim, fontPtr, fontPtr.FontSize, float.MaxValue, 0, pTest, null, null); + Log.Information($"{nameof(GamePrebakedFontsTestWidget)}: {handle} => {dim}"); + } + } } diff --git a/Dalamud/Interface/ManagedFontAtlas/IFontHandle.cs b/Dalamud/Interface/ManagedFontAtlas/IFontHandle.cs index 460fd53a0..81ce84a63 100644 --- a/Dalamud/Interface/ManagedFontAtlas/IFontHandle.cs +++ b/Dalamud/Interface/ManagedFontAtlas/IFontHandle.cs @@ -1,4 +1,6 @@ -using Dalamud.Utility; +using System.Threading.Tasks; + +using Dalamud.Utility; using ImGuiNET; @@ -9,6 +11,11 @@ namespace Dalamud.Interface.ManagedFontAtlas; /// public interface IFontHandle : IDisposable { + /// + /// Called when the built instance of has been changed. + /// + event Action ImFontChanged; + /// /// Represents a reference counting handle for fonts. Dalamud internal use only. /// @@ -18,7 +25,8 @@ public interface IFontHandle : IDisposable /// 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. + /// Futures changes may make simple not enough.
+ /// If you need to access a font outside the UI thread, consider using . ///
ImFontPtr ImFont { get; } } @@ -29,11 +37,27 @@ public interface IFontHandle : IDisposable 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. + /// Gets a value indicating whether this font is ready for use. ///
+ /// + /// Once set to true, it will remain true.
+ /// Use directly if you want to keep the current ImGui font if the font is not ready.
+ /// Alternatively, use to wait for this property to become true. + ///
bool Available { get; } + /// + /// Locks the fully constructed instance of corresponding to the this + /// , for read-only use in any thread. + /// + /// An instance of that must be disposed after use. + /// + /// Calling . will not unlock the + /// locked by this function. + /// + /// If is false. + ImFontLocked Lock(); + /// /// Pushes the current font into ImGui font stack using , if available.
/// Use to access the current font.
@@ -47,6 +71,54 @@ public interface IFontHandle : IDisposable /// FontPopper Push(); + /// + /// Waits for to become true. + /// + /// A task containing this . + Task WaitAsync(); + + /// + /// The wrapper for , guaranteeing that the associated data will be available as long as + /// this struct is not disposed. + /// + public struct ImFontLocked : IDisposable + { + /// + /// The associated . + /// + public ImFontPtr ImFont; + + private IRefCountable? owner; + + /// + /// Initializes a new instance of the struct, + /// and incrase the reference count of . + /// + /// The contained font. + /// The owner. + internal ImFontLocked(ImFontPtr imFont, IRefCountable owner) + { + owner.AddRef(); + this.ImFont = imFont; + this.owner = owner; + } + + public static implicit operator ImFontPtr(ImFontLocked l) => l.ImFont; + + public static unsafe implicit operator ImFont*(ImFontLocked l) => l.ImFont.NativePtr; + + /// + public void Dispose() + { + if (this.owner is null) + return; + + this.owner.Release(); + this.owner = null; + this.ImFont = default; + } + } + /// /// The wrapper for popping fonts. /// diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/DelegateFontHandle.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/DelegateFontHandle.cs index bde349736..f50967fae 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/DelegateFontHandle.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/DelegateFontHandle.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Linq; +using System.Threading.Tasks; using Dalamud.Interface.Utility; using Dalamud.Logging.Internal; @@ -27,6 +28,11 @@ internal class DelegateFontHandle : IFontHandle.IInternal this.CallOnBuildStepChange = callOnBuildStepChange; } + /// + public event Action? ImFontChanged; + + private event Action? Disposed; + /// /// Gets the function to be called on build step changes. /// @@ -49,11 +55,76 @@ internal class DelegateFontHandle : IFontHandle.IInternal { this.manager?.FreeFontHandle(this); this.manager = null; + this.Disposed?.InvokeSafely(this); + this.ImFontChanged = null; + } + + /// + public IFontHandle.ImFontLocked Lock() + { + IFontHandleSubstance? prevSubstance = default; + while (true) + { + var substance = this.ManagerNotDisposed.Substance; + if (substance is null) + throw new InvalidOperationException(); + if (substance == prevSubstance) + throw new ObjectDisposedException(nameof(DelegateFontHandle)); + + prevSubstance = substance; + try + { + substance.DataRoot.AddRef(); + } + catch (ObjectDisposedException) + { + continue; + } + + try + { + var fontPtr = substance.GetFontPtr(this); + if (fontPtr.IsNull()) + continue; + return new(fontPtr, substance.DataRoot); + } + finally + { + substance.DataRoot.Release(); + } + } } /// public IFontHandle.FontPopper Push() => new(this.ImFont, this.Available); + /// + public Task WaitAsync() + { + if (this.Available) + return Task.FromResult(this); + + var tcs = new TaskCompletionSource(); + this.ImFontChanged += OnImFontChanged; + this.Disposed += OnImFontChanged; + if (this.Available) + OnImFontChanged(this); + return tcs.Task; + + void OnImFontChanged(IFontHandle unused) + { + if (tcs.Task.IsCompletedSuccessfully) + return; + + this.ImFontChanged -= OnImFontChanged; + this.Disposed -= OnImFontChanged; + if (this.manager is null) + tcs.SetException(new ObjectDisposedException(nameof(GamePrebakedFontHandle))); + else + tcs.SetResult(this); + } + } + /// /// Manager for s. /// @@ -81,11 +152,7 @@ internal class DelegateFontHandle : IFontHandle.IInternal public void Dispose() { lock (this.syncRoot) - { this.handles.Clear(); - this.Substance?.Dispose(); - this.Substance = null; - } } /// @@ -109,10 +176,20 @@ internal class DelegateFontHandle : IFontHandle.IInternal } /// - public IFontHandleSubstance NewSubstance() + public void InvokeFontHandleImFontChanged() + { + if (this.Substance is not HandleSubstance hs) + return; + + foreach (var handle in hs.RelevantHandles) + handle.ImFontChanged?.InvokeSafely(handle); + } + + /// + public IFontHandleSubstance NewSubstance(IRefCountable dataRoot) { lock (this.syncRoot) - return new HandleSubstance(this, this.handles.ToArray()); + return new HandleSubstance(this, dataRoot, this.handles.ToArray()); } } @@ -123,9 +200,6 @@ internal class DelegateFontHandle : IFontHandle.IInternal { 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(); @@ -134,13 +208,29 @@ internal class DelegateFontHandle : IFontHandle.IInternal /// Initializes a new instance of the class. ///
/// The manager. + /// The data root. /// The relevant handles. - public HandleSubstance(IFontHandleManager manager, DelegateFontHandle[] relevantHandles) + public HandleSubstance( + IFontHandleManager manager, + IRefCountable dataRoot, + DelegateFontHandle[] relevantHandles) { + // We do not call dataRoot.AddRef; this object is dependant on lifetime of dataRoot. + this.Manager = manager; - this.relevantHandles = relevantHandles; + this.DataRoot = dataRoot; + this.RelevantHandles = relevantHandles; } + /// + /// Gets the relevant handles. + /// + // Not owned by this class. Do not dispose. + public DelegateFontHandle[] RelevantHandles { get; } + + /// + public IRefCountable DataRoot { get; } + /// public IFontHandleManager Manager { get; } @@ -171,7 +261,7 @@ internal class DelegateFontHandle : IFontHandle.IInternal public void OnPreBuild(IFontAtlasBuildToolkitPreBuild toolkitPreBuild) { var fontsVector = toolkitPreBuild.Fonts; - foreach (var k in this.relevantHandles) + foreach (var k in this.RelevantHandles) { var fontCountPrevious = fontsVector.Length; @@ -288,7 +378,7 @@ internal class DelegateFontHandle : IFontHandle.IInternal /// public void OnPostBuild(IFontAtlasBuildToolkitPostBuild toolkitPostBuild) { - foreach (var k in this.relevantHandles) + foreach (var k in this.RelevantHandles) { if (!this.fonts[k].IsNotNullAndLoaded()) continue; @@ -315,7 +405,7 @@ internal class DelegateFontHandle : IFontHandle.IInternal /// public void OnPostPromotion(IFontAtlasBuildToolkitPostPromotion toolkitPostPromotion) { - foreach (var k in this.relevantHandles) + foreach (var k in this.RelevantHandles) { if (!this.fonts[k].IsNotNullAndLoaded()) continue; diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs index eddccfa76..99ce8dab9 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs @@ -43,68 +43,67 @@ internal sealed partial class FontAtlasFactory private static readonly Task EmptyTask = Task.FromResult(default(FontAtlasBuiltData)); - private struct FontAtlasBuiltData : IDisposable + private class FontAtlasBuiltData : IRefCountable { - public readonly DalamudFontAtlas? Owner; - public readonly ImFontAtlasPtr Atlas; - public readonly float Scale; + private readonly List wraps; + private readonly List substances; - public bool IsBuildInProgress; + private int refCount; - private readonly List? wraps; - private readonly List? substances; - private readonly DisposeSafety.ScopedFinalizer? garbage; - - public unsafe FontAtlasBuiltData( - DalamudFontAtlas owner, - IEnumerable substances, - float scale) + public unsafe FontAtlasBuiltData(DalamudFontAtlas owner, float scale) { this.Owner = owner; this.Scale = scale; - this.garbage = new(); + this.Garbage = new(); + this.refCount = 1; try { var substancesList = this.substances = new(); - foreach (var s in substances) - substancesList.Add(this.garbage.Add(s)); - this.garbage.Add(() => substancesList.Clear()); + this.Garbage.Add(() => substancesList.Clear()); var wrapsCopy = this.wraps = new(); - this.garbage.Add(() => wrapsCopy.Clear()); + 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.Garbage.Add(() => ImGuiNative.ImFontAtlas_destroy(atlasPtr)); this.IsBuildInProgress = true; } catch { - this.garbage.Dispose(); + this.Garbage.Dispose(); throw; } } - public readonly DisposeSafety.ScopedFinalizer Garbage => - this.garbage ?? throw new ObjectDisposedException(nameof(FontAtlasBuiltData)); + public DalamudFontAtlas? Owner { get; } - public readonly ImVectorWrapper Fonts => this.Atlas.FontsWrapped(); + public ImFontAtlasPtr Atlas { get; } - public readonly ImVectorWrapper ConfigData => this.Atlas.ConfigDataWrapped(); + public float Scale { get; } - public readonly ImVectorWrapper ImTextures => this.Atlas.TexturesWrapped(); + public bool IsBuildInProgress { get; set; } - public readonly IReadOnlyList Wraps => - (IReadOnlyList?)this.wraps ?? Array.Empty(); + public DisposeSafety.ScopedFinalizer Garbage { get; } - public readonly IReadOnlyList Substances => - (IReadOnlyList?)this.substances ?? Array.Empty(); + public ImVectorWrapper Fonts => this.Atlas.FontsWrapped(); - public readonly void AddExistingTexture(IDalamudTextureWrap wrap) + public ImVectorWrapper ConfigData => this.Atlas.ConfigDataWrapped(); + + public ImVectorWrapper ImTextures => this.Atlas.TexturesWrapped(); + + public IReadOnlyList Wraps => this.wraps; + + public IReadOnlyList Substances => this.substances; + + public void InitialAddSubstance(IFontHandleSubstance substance) => + this.substances.Add(this.Garbage.Add(substance)); + + public void AddExistingTexture(IDalamudTextureWrap wrap) { if (this.wraps is null) throw new ObjectDisposedException(nameof(FontAtlasBuiltData)); @@ -112,7 +111,7 @@ internal sealed partial class FontAtlasFactory this.wraps.Add(this.Garbage.Add(wrap)); } - public readonly int AddNewTexture(IDalamudTextureWrap wrap, bool disposeOnError) + public int AddNewTexture(IDalamudTextureWrap wrap, bool disposeOnError) { if (this.wraps is null) throw new ObjectDisposedException(nameof(FontAtlasBuiltData)); @@ -160,27 +159,47 @@ internal sealed partial class FontAtlasFactory return index; } - public unsafe void Dispose() + public int AddRef() => IRefCountable.AlterRefCount(1, ref this.refCount, out var newRefCount) switch { - if (this.garbage is null) - return; + IRefCountable.RefCountResult.StillAlive => newRefCount, + IRefCountable.RefCountResult.AlreadyDisposed => + throw new ObjectDisposedException(nameof(FontAtlasBuiltData)), + IRefCountable.RefCountResult.FinalRelease => throw new InvalidOperationException(), + _ => throw new InvalidOperationException(), + }; - if (this.IsBuildInProgress) + public unsafe int Release() + { + switch (IRefCountable.AlterRefCount(-1, ref this.refCount, out var newRefCount)) { - 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); - } + case IRefCountable.RefCountResult.StillAlive: + return newRefCount; + + case IRefCountable.RefCountResult.FinalRelease: + 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); + Log.Verbose("[{name}] 0x{ptr:X}: Disposing", this.Owner?.Name ?? "", (nint)this.Atlas.NativePtr); #endif - this.garbage.Dispose(); + this.Garbage.Dispose(); + return newRefCount; + + case IRefCountable.RefCountResult.AlreadyDisposed: + throw new ObjectDisposedException(nameof(FontAtlasBuiltData)); + + default: + throw new InvalidOperationException(); + } } public BuildToolkit CreateToolkit(FontAtlasFactory factory, bool isAsync) @@ -201,8 +220,8 @@ internal sealed partial class FontAtlasFactory private readonly object syncRootPostPromotion = new(); private readonly object syncRoot = new(); - private Task buildTask = EmptyTask; - private FontAtlasBuiltData builtData; + private Task buildTask = EmptyTask; + private FontAtlasBuiltData? builtData; private int buildSuppressionCounter; private bool buildSuppressionSuppressed; @@ -275,7 +294,8 @@ internal sealed partial class FontAtlasFactory lock (this.syncRoot) { this.buildTask.ToDisposableIgnoreExceptions().Dispose(); - this.builtData.Dispose(); + this.builtData?.Release(); + this.builtData = null; } } @@ -303,7 +323,7 @@ internal sealed partial class FontAtlasFactory get { lock (this.syncRoot) - return this.builtData.Atlas; + return this.builtData?.Atlas ?? default; } } @@ -311,7 +331,7 @@ internal sealed partial class FontAtlasFactory public Task BuildTask => this.buildTask; /// - public bool HasBuiltAtlas => !this.builtData.Atlas.IsNull(); + public bool HasBuiltAtlas => !(this.builtData?.Atlas.IsNull() ?? true); /// public bool IsGlobalScaled { get; } @@ -474,13 +494,13 @@ internal sealed partial class FontAtlasFactory var rebuildIndex = ++this.buildIndex; return this.buildTask = this.buildTask.ContinueWith(BuildInner).Unwrap(); - async Task BuildInner(Task unused) + 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; + return null; } var res = await this.RebuildFontsPrivate(true, scale); @@ -512,8 +532,10 @@ internal sealed partial class FontAtlasFactory return; } - this.builtData.ExplicitDisposeIgnoreExceptions(); + var prevBuiltData = this.builtData; this.builtData = data; + prevBuiltData.ExplicitDisposeIgnoreExceptions(); + this.buildTask = EmptyTask; foreach (var substance in data.Substances) substance.Manager.Substance = substance; @@ -570,6 +592,9 @@ internal sealed partial class FontAtlasFactory } } + foreach (var substance in data.Substances) + substance.Manager.InvokeFontHandleImFontChanged(); + #if VeryVerboseLog Log.Verbose("[{name}] Built from {source}.", this.Name, source); #endif @@ -610,12 +635,14 @@ internal sealed partial class FontAtlasFactory var sw = new Stopwatch(); sw.Start(); - var res = default(FontAtlasBuiltData); + FontAtlasBuiltData? res = null; nint atlasPtr = 0; BuildToolkit? toolkit = null; try { - res = new(this, this.fontHandleManagers.Select(x => x.NewSubstance()), scale); + res = new(this, scale); + foreach (var fhm in this.fontHandleManagers) + res.InitialAddSubstance(fhm.NewSubstance(res)); unsafe { atlasPtr = (nint)res.Atlas.NativePtr; @@ -646,9 +673,11 @@ internal sealed partial class FontAtlasFactory res.IsBuildInProgress = false; toolkit.Dispose(); - res.Dispose(); + res.Release(); - res = new(this, this.fontHandleManagers.Select(x => x.NewSubstance()), scale); + res = new(this, scale); + foreach (var fhm in this.fontHandleManagers) + res.InitialAddSubstance(fhm.NewSubstance(res)); unsafe { atlasPtr = (nint)res.Atlas.NativePtr; @@ -715,8 +744,12 @@ internal sealed partial class FontAtlasFactory nameof(this.RebuildFontsPrivateReal), atlasPtr, sw.ElapsedMilliseconds); - res.IsBuildInProgress = false; - res.Dispose(); + if (res is not null) + { + res.IsBuildInProgress = false; + res.Release(); + } + throw; } finally diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/GamePrebakedFontHandle.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/GamePrebakedFontHandle.cs index feda47a8a..c05b3a96d 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/GamePrebakedFontHandle.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/GamePrebakedFontHandle.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Reactive.Disposables; +using System.Threading.Tasks; using Dalamud.Game.Text; using Dalamud.Interface.GameFonts; @@ -53,6 +54,11 @@ internal class GamePrebakedFontHandle : IFontHandle.IInternal this.FontStyle = style; } + /// + public event Action? ImFontChanged; + + private event Action? Disposed; + /// /// Provider for for `common/font/fontNN.tex`. /// @@ -113,17 +119,86 @@ internal class GamePrebakedFontHandle : IFontHandle.IInternal { this.manager?.FreeFontHandle(this); this.manager = null; + this.Disposed?.InvokeSafely(this); + this.ImFontChanged = null; + } + + /// + public IFontHandle.ImFontLocked Lock() + { + IFontHandleSubstance? prevSubstance = default; + while (true) + { + var substance = this.ManagerNotDisposed.Substance; + if (substance is null) + throw new InvalidOperationException(); + if (substance == prevSubstance) + throw new ObjectDisposedException(nameof(DelegateFontHandle)); + + prevSubstance = substance; + try + { + substance.DataRoot.AddRef(); + } + catch (ObjectDisposedException) + { + continue; + } + + try + { + var fontPtr = substance.GetFontPtr(this); + if (fontPtr.IsNull()) + continue; + return new(fontPtr, substance.DataRoot); + } + finally + { + substance.DataRoot.Release(); + } + } } /// public IFontHandle.FontPopper Push() => new(this.ImFont, this.Available); + /// + public Task WaitAsync() + { + if (this.Available) + return Task.FromResult(this); + + var tcs = new TaskCompletionSource(); + this.ImFontChanged += OnImFontChanged; + this.Disposed += OnImFontChanged; + if (this.Available) + OnImFontChanged(this); + return tcs.Task; + + void OnImFontChanged(IFontHandle unused) + { + if (tcs.Task.IsCompletedSuccessfully) + return; + + this.ImFontChanged -= OnImFontChanged; + this.Disposed -= OnImFontChanged; + if (this.manager is null) + tcs.SetException(new ObjectDisposedException(nameof(GamePrebakedFontHandle))); + else + tcs.SetResult(this); + } + } + + /// + public override string ToString() => $"{nameof(GamePrebakedFontHandle)}({this.FontStyle})"; + /// /// Manager for s. /// internal sealed class HandleManager : IFontHandleManager { private readonly Dictionary gameFontsRc = new(); + private readonly HashSet handles = new(); private readonly object syncRoot = new(); /// @@ -154,8 +229,7 @@ internal class GamePrebakedFontHandle : IFontHandle.IInternal /// public void Dispose() { - this.Substance?.Dispose(); - this.Substance = null; + // empty } /// @@ -165,6 +239,7 @@ internal class GamePrebakedFontHandle : IFontHandle.IInternal bool suggestRebuild; lock (this.syncRoot) { + this.handles.Add(handle); this.gameFontsRc[style] = this.gameFontsRc.GetValueOrDefault(style, 0) + 1; suggestRebuild = this.Substance?.GetFontPtr(handle).IsNotNullAndLoaded() is not true; } @@ -183,6 +258,7 @@ internal class GamePrebakedFontHandle : IFontHandle.IInternal lock (this.syncRoot) { + this.handles.Remove(ggfh); if (!this.gameFontsRc.ContainsKey(ggfh.FontStyle)) return; @@ -192,10 +268,20 @@ internal class GamePrebakedFontHandle : IFontHandle.IInternal } /// - public IFontHandleSubstance NewSubstance() + public void InvokeFontHandleImFontChanged() + { + if (this.Substance is not HandleSubstance hs) + return; + + foreach (var handle in hs.RelevantHandles) + handle.ImFontChanged?.InvokeSafely(handle); + } + + /// + public IFontHandleSubstance NewSubstance(IRefCountable dataRoot) { lock (this.syncRoot) - return new HandleSubstance(this, this.gameFontsRc.Keys); + return new HandleSubstance(this, dataRoot, this.handles.ToArray(), this.gameFontsRc.Keys); } } @@ -218,14 +304,32 @@ internal class GamePrebakedFontHandle : IFontHandle.IInternal /// Initializes a new instance of the class. /// /// The manager. + /// The data root. + /// The relevant handles. /// The game font styles. - public HandleSubstance(HandleManager manager, IEnumerable gameFontStyles) + public HandleSubstance( + HandleManager manager, + IRefCountable dataRoot, + GamePrebakedFontHandle[] relevantHandles, + IEnumerable gameFontStyles) { + // We do not call dataRoot.AddRef; this object is dependant on lifetime of dataRoot. + this.handleManager = manager; - Service.Get(); + this.DataRoot = dataRoot; + this.RelevantHandles = relevantHandles; this.gameFontStyles = new(gameFontStyles); } + /// + /// Gets the relevant handles. + /// + // Not owned by this class. Do not dispose. + public GamePrebakedFontHandle[] RelevantHandles { get; } + + /// + public IRefCountable DataRoot { get; } + /// public IFontHandleManager Manager => this.handleManager; @@ -240,6 +344,7 @@ internal class GamePrebakedFontHandle : IFontHandle.IInternal /// public void Dispose() { + // empty } /// diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/IFontHandleManager.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/IFontHandleManager.cs index 93c688608..7066817b7 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/IFontHandleManager.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/IFontHandleManager.cs @@ -1,3 +1,5 @@ +using Dalamud.Utility; + namespace Dalamud.Interface.ManagedFontAtlas.Internals; /// @@ -27,6 +29,12 @@ internal interface IFontHandleManager : IDisposable /// /// Creates a new substance of the font atlas. /// + /// The data root. /// The new substance. - IFontHandleSubstance NewSubstance(); + IFontHandleSubstance NewSubstance(IRefCountable dataRoot); + + /// + /// Invokes . + /// + void InvokeFontHandleImFontChanged(); } diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/IFontHandleSubstance.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/IFontHandleSubstance.cs index c800c30ac..73c14efc1 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/IFontHandleSubstance.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/IFontHandleSubstance.cs @@ -9,6 +9,11 @@ namespace Dalamud.Interface.ManagedFontAtlas.Internals; /// internal interface IFontHandleSubstance : IDisposable { + /// + /// Gets the data root relevant to this instance of . + /// + IRefCountable DataRoot { get; } + /// /// Gets the manager relevant to this instance of . /// diff --git a/Dalamud/Utility/IRefCountable.cs b/Dalamud/Utility/IRefCountable.cs new file mode 100644 index 000000000..76d1059d1 --- /dev/null +++ b/Dalamud/Utility/IRefCountable.cs @@ -0,0 +1,77 @@ +using System.Diagnostics; +using System.Threading; + +namespace Dalamud.Utility; + +/// +/// Interface for reference counting. +/// +internal interface IRefCountable : IDisposable +{ + /// + /// Result for . + /// + public enum RefCountResult + { + /// + /// The object still has remaining references. No futher action should be done. + /// + StillAlive = 1, + + /// + /// The last reference to the object has been released. The object should be fully released. + /// + FinalRelease = 2, + + /// + /// The object already has been disposed. may be thrown. + /// + AlreadyDisposed = 3, + } + + /// + /// Adds a reference to this reference counted object. + /// + /// The new number of references. + int AddRef(); + + /// + /// Releases a reference from this reference counted object.
+ /// When all references are released, the object will be fully disposed. + ///
+ /// The new number of references. + int Release(); + + /// + /// Alias for . + /// + void IDisposable.Dispose() => this.Release(); + + /// + /// Alters by . + /// + /// The delta to the reference count. + /// The reference to the reference count. + /// The new reference count. + /// The followup action that should be done. + public static RefCountResult AlterRefCount(int delta, ref int refCount, out int newRefCount) + { + Debug.Assert(delta is 1 or -1, "delta must be 1 or -1"); + + while (true) + { + var refCountCopy = refCount; + if (refCountCopy <= 0) + { + newRefCount = refCountCopy; + return RefCountResult.AlreadyDisposed; + } + + newRefCount = refCountCopy + delta; + if (refCountCopy != Interlocked.CompareExchange(ref refCount, newRefCount, refCountCopy)) + continue; + + return newRefCount == 0 ? RefCountResult.FinalRelease : RefCountResult.StillAlive; + } + } +} From 967ae973084e843d8313df7e7458d2cffa678459 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Sun, 21 Jan 2024 03:41:26 +0900 Subject: [PATCH 2/9] Expose wrapped default font handle --- .../Interface/Internal/InterfaceManager.cs | 35 ++++-- .../Interface/ManagedFontAtlas/IFontAtlas.cs | 4 + .../Interface/ManagedFontAtlas/IFontHandle.cs | 7 +- Dalamud/Interface/UiBuilder.cs | 106 ++++++++++++++++-- 4 files changed, 131 insertions(+), 21 deletions(-) diff --git a/Dalamud/Interface/Internal/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs index 8915b3e3d..62f9145bf 100644 --- a/Dalamud/Interface/Internal/InterfaceManager.cs +++ b/Dalamud/Interface/Internal/InterfaceManager.cs @@ -13,7 +13,6 @@ using Dalamud.Game.ClientState.Keys; using Dalamud.Game.Internal.DXGI; using Dalamud.Hooking; using Dalamud.Hooking.WndProcHook; -using Dalamud.Interface.GameFonts; using Dalamud.Interface.Internal.ManagedAsserts; using Dalamud.Interface.Internal.Notifications; using Dalamud.Interface.ManagedFontAtlas; @@ -87,9 +86,6 @@ internal class InterfaceManager : IDisposable, IServiceType 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; @@ -131,19 +127,34 @@ internal class InterfaceManager : IDisposable, IServiceType /// Gets the default ImGui font.
/// Accessing this static property outside of the main thread is dangerous and not supported. ///
- public static ImFontPtr DefaultFont => WhenFontsReady().defaultFontHandle!.ImFont.OrElse(ImGui.GetIO().FontDefault); + public static ImFontPtr DefaultFont => WhenFontsReady().DefaultFontHandle!.ImFont.OrElse(ImGui.GetIO().FontDefault); /// /// Gets an included FontAwesome icon font.
/// Accessing this static property outside of the main thread is dangerous and not supported. ///
- public static ImFontPtr IconFont => WhenFontsReady().iconFontHandle!.ImFont.OrElse(ImGui.GetIO().FontDefault); + public static ImFontPtr IconFont => WhenFontsReady().IconFontHandle!.ImFont.OrElse(ImGui.GetIO().FontDefault); /// /// Gets an included monospaced font.
/// Accessing this static property outside of the main thread is dangerous and not supported. ///
- public static ImFontPtr MonoFont => WhenFontsReady().monoFontHandle!.ImFont.OrElse(ImGui.GetIO().FontDefault); + public static ImFontPtr MonoFont => WhenFontsReady().MonoFontHandle!.ImFont.OrElse(ImGui.GetIO().FontDefault); + + /// + /// Gets the default font handle. + /// + public IFontHandle.IInternal? DefaultFontHandle { get; private set; } + + /// + /// Gets the icon font handle. + /// + public IFontHandle.IInternal? IconFontHandle { get; private set; } + + /// + /// Gets the mono font handle. + /// + public IFontHandle.IInternal? MonoFontHandle { get; private set; } /// /// Gets or sets the pointer to ImGui.IO(), when it was last used. @@ -691,9 +702,9 @@ internal class InterfaceManager : IDisposable, IServiceType .CreateFontAtlas(nameof(InterfaceManager), FontAtlasAutoRebuildMode.Disable); using (this.dalamudAtlas.SuppressAutoRebuild()) { - this.defaultFontHandle = (IFontHandle.IInternal)this.dalamudAtlas.NewDelegateFontHandle( + this.DefaultFontHandle = (IFontHandle.IInternal)this.dalamudAtlas.NewDelegateFontHandle( e => e.OnPreBuild(tk => tk.AddDalamudDefaultFont(DefaultFontSizePx))); - this.iconFontHandle = (IFontHandle.IInternal)this.dalamudAtlas.NewDelegateFontHandle( + this.IconFontHandle = (IFontHandle.IInternal)this.dalamudAtlas.NewDelegateFontHandle( e => e.OnPreBuild( tk => tk.AddFontAwesomeIconFont( new() @@ -702,7 +713,7 @@ internal class InterfaceManager : IDisposable, IServiceType GlyphMinAdvanceX = DefaultFontSizePx, GlyphMaxAdvanceX = DefaultFontSizePx, }))); - this.monoFontHandle = (IFontHandle.IInternal)this.dalamudAtlas.NewDelegateFontHandle( + this.MonoFontHandle = (IFontHandle.IInternal)this.dalamudAtlas.NewDelegateFontHandle( e => e.OnPreBuild( tk => tk.AddDalamudAssetFont( DalamudAsset.InconsolataRegular, @@ -715,12 +726,12 @@ internal class InterfaceManager : IDisposable, IServiceType // Use font handles directly. // Fill missing glyphs in MonoFont from DefaultFont - tk.CopyGlyphsAcrossFonts(this.defaultFontHandle.ImFont, this.monoFontHandle.ImFont, true); + tk.CopyGlyphsAcrossFonts(this.DefaultFontHandle.ImFont, this.MonoFontHandle.ImFont, true); // Update default font unsafe { - ImGui.GetIO().NativePtr->FontDefault = this.defaultFontHandle.ImFont; + ImGui.GetIO().NativePtr->FontDefault = this.DefaultFontHandle.ImFont; } // Broadcast to auto-rebuilding instances diff --git a/Dalamud/Interface/ManagedFontAtlas/IFontAtlas.cs b/Dalamud/Interface/ManagedFontAtlas/IFontAtlas.cs index ec3e66e9a..491292f9d 100644 --- a/Dalamud/Interface/ManagedFontAtlas/IFontAtlas.cs +++ b/Dalamud/Interface/ManagedFontAtlas/IFontAtlas.cs @@ -122,6 +122,10 @@ public interface IFontAtlas : IDisposable /// Note that would not necessarily get changed from calling this function. /// /// If is . + /// + /// Using this method will block the main thread on rebuilding fonts, effectively calling + /// from the main thread. Consider migrating to . + /// void BuildFontsOnNextFrame(); /// diff --git a/Dalamud/Interface/ManagedFontAtlas/IFontHandle.cs b/Dalamud/Interface/ManagedFontAtlas/IFontHandle.cs index 81ce84a63..eb57b815f 100644 --- a/Dalamud/Interface/ManagedFontAtlas/IFontHandle.cs +++ b/Dalamud/Interface/ManagedFontAtlas/IFontHandle.cs @@ -12,7 +12,12 @@ namespace Dalamud.Interface.ManagedFontAtlas; public interface IFontHandle : IDisposable { /// - /// Called when the built instance of has been changed. + /// Called when the built instance of has been changed.
+ /// This event will be invoked on the same thread with + /// ., + /// when the build step is .
+ /// See , , and + /// . ///
event Action ImFontChanged; diff --git a/Dalamud/Interface/UiBuilder.cs b/Dalamud/Interface/UiBuilder.cs index 87e3b9032..43912f224 100644 --- a/Dalamud/Interface/UiBuilder.cs +++ b/Dalamud/Interface/UiBuilder.cs @@ -41,6 +41,10 @@ public sealed class UiBuilder : IDisposable private bool hasErrorWindow = false; private bool lastFrameUiHideState = false; + private IFontHandle? defaultFontHandle; + private IFontHandle? iconFontHandle; + private IFontHandle? monoFontHandle; + /// /// Initializes a new instance of the class and registers it. /// You do not have to call this manually. @@ -103,7 +107,14 @@ public sealed class UiBuilder : IDisposable /// (at any time), so you should both reload your custom fonts and restore those /// pointers inside this handler. /// - [Obsolete($"Use {nameof(this.FontAtlas)} instead.", false)] + /// + /// To add your custom font, use . or + /// .
+ /// To be notified on font changes after fonts are built, use + /// ..
+ /// For all other purposes, use .. + ///
+ [Obsolete("See remarks.", false)] [Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)] public event Action? BuildFonts; @@ -113,6 +124,13 @@ public sealed class UiBuilder : IDisposable /// (at any time), so you should both reload your custom fonts and restore those /// pointers inside this handler. ///
+ /// + /// To add your custom font, use . or + /// .
+ /// To be notified on font changes after fonts are built, use + /// ..
+ /// For all other purposes, use .. + ///
[Obsolete($"Use {nameof(this.FontAtlas)} instead.", false)] [Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)] public event Action? AfterBuildFonts; @@ -143,6 +161,23 @@ public sealed class UiBuilder : IDisposable /// Gets the default Dalamud font - supporting all game languages and icons.
/// Accessing this static property outside of is dangerous and not supported. /// + public static ImFontPtr DefaultFont => InterfaceManager.DefaultFont; + + /// + /// Gets the default Dalamud icon font based on FontAwesome 5 Free solid.
+ /// Accessing this static property outside of is dangerous and not supported. + ///
+ public static ImFontPtr IconFont => InterfaceManager.IconFont; + + /// + /// Gets the default Dalamud monospaced font based on Inconsolata Regular.
+ /// Accessing this static property outside of is dangerous and not supported. + ///
+ public static ImFontPtr MonoFont => InterfaceManager.MonoFont; + + /// + /// Gets the handle to the default Dalamud font - supporting all game languages and icons. + /// /// /// A font handle corresponding to this font can be obtained with: /// @@ -151,11 +186,15 @@ public sealed class UiBuilder : IDisposable /// tk => tk.AddDalamudDefaultFont(UiBuilder.DefaultFontSizePt))); /// /// - public static ImFontPtr DefaultFont => InterfaceManager.DefaultFont; + public IFontHandle DefaultFontHandle => + this.defaultFontHandle ??= + this.scopedFinalizer.Add( + new FontHandleWrapper( + this.InterfaceManagerWithScene?.DefaultFontHandle + ?? throw new InvalidOperationException("Scene is not yet ready."))); /// - /// Gets the default Dalamud icon font based on FontAwesome 5 Free solid.
- /// Accessing this static property outside of is dangerous and not supported. + /// Gets the default Dalamud icon font based on FontAwesome 5 Free solid. ///
/// /// A font handle corresponding to this font can be obtained with: @@ -165,11 +204,15 @@ public sealed class UiBuilder : IDisposable /// tk => tk.AddFontAwesomeIconFont(new() { SizePt = UiBuilder.DefaultFontSizePt }))); /// /// - public static ImFontPtr IconFont => InterfaceManager.IconFont; + public IFontHandle IconFontHandle => + this.iconFontHandle ??= + this.scopedFinalizer.Add( + new FontHandleWrapper( + this.InterfaceManagerWithScene?.IconFontHandle + ?? throw new InvalidOperationException("Scene is not yet ready."))); /// - /// Gets the default Dalamud monospaced font based on Inconsolata Regular.
- /// Accessing this static property outside of is dangerous and not supported. + /// Gets the default Dalamud monospaced font based on Inconsolata Regular. ///
/// /// A font handle corresponding to this font can be obtained with: @@ -181,7 +224,12 @@ public sealed class UiBuilder : IDisposable /// new() { SizePt = UiBuilder.DefaultFontSizePt }))); /// /// - public static ImFontPtr MonoFont => InterfaceManager.MonoFont; + public IFontHandle MonoFontHandle => + this.monoFontHandle ??= + this.scopedFinalizer.Add( + new FontHandleWrapper( + this.InterfaceManagerWithScene?.MonoFontHandle + ?? throw new InvalidOperationException("Scene is not yet ready."))); /// /// Gets the game's active Direct3D device. @@ -660,4 +708,46 @@ public sealed class UiBuilder : IDisposable { this.ResizeBuffers?.InvokeSafely(); } + + private class FontHandleWrapper : IFontHandle + { + private IFontHandle? wrapped; + + public FontHandleWrapper(IFontHandle wrapped) + { + this.wrapped = wrapped; + this.wrapped.ImFontChanged += this.WrappedOnImFontChanged; + } + + public event Action? ImFontChanged; + + public Exception? LoadException => + this.wrapped!.LoadException ?? new ObjectDisposedException(nameof(FontHandleWrapper)); + + public bool Available => this.wrapped?.Available ?? false; + + public void Dispose() + { + if (this.wrapped is not { } w) + return; + + this.wrapped = null; + w.ImFontChanged -= this.WrappedOnImFontChanged; + // Note: do not dispose w; we do not own it + } + + public IFontHandle.ImFontLocked Lock() => + this.wrapped?.Lock() ?? throw new ObjectDisposedException(nameof(FontHandleWrapper)); + + public IFontHandle.FontPopper Push() => + this.wrapped?.Push() ?? throw new ObjectDisposedException(nameof(FontHandleWrapper)); + + public Task WaitAsync() => + this.wrapped?.WaitAsync().ContinueWith(_ => (IFontHandle)this) ?? + throw new ObjectDisposedException(nameof(FontHandleWrapper)); + + public override string ToString() => $"{nameof(FontHandleWrapper)}({this.wrapped})"; + + private void WrappedOnImFontChanged(IFontHandle obj) => this.ImFontChanged.InvokeSafely(this); + } } From 0701d7805a94723eb8ec8a948c5e5279325e922f Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Sun, 21 Jan 2024 04:07:21 +0900 Subject: [PATCH 3/9] BuildFonts remarks --- Dalamud/Interface/UiBuilder.cs | 45 +++++++++++++++++++++++++--------- 1 file changed, 34 insertions(+), 11 deletions(-) diff --git a/Dalamud/Interface/UiBuilder.cs b/Dalamud/Interface/UiBuilder.cs index 43912f224..02decf103 100644 --- a/Dalamud/Interface/UiBuilder.cs +++ b/Dalamud/Interface/UiBuilder.cs @@ -13,6 +13,7 @@ using Dalamud.Interface.Internal.ManagedAsserts; using Dalamud.Interface.Internal.Notifications; using Dalamud.Interface.ManagedFontAtlas; using Dalamud.Interface.ManagedFontAtlas.Internals; +using Dalamud.Plugin.Internal.Types; using Dalamud.Utility; using ImGuiNET; using ImGuiScene; @@ -103,7 +104,7 @@ public sealed class UiBuilder : IDisposable /// /// Gets or sets an action that is called any time ImGui fonts need to be rebuilt.
- /// Any ImFontPtr objects that you store can be invalidated when fonts are rebuilt + /// Any ImFontPtr objects that you store can be invalidated when fonts are rebuilt /// (at any time), so you should both reload your custom fonts and restore those /// pointers inside this handler. ///
@@ -112,7 +113,36 @@ public sealed class UiBuilder : IDisposable /// .
/// To be notified on font changes after fonts are built, use /// ..
- /// For all other purposes, use .. + /// For all other purposes, use ..
+ ///
+ /// Note that you will be calling above functions once, instead of every time inside a build step change callback. + /// For example, you can make all font handles from your plugin constructor, and then use the created handles during + /// event, by using in a scope.
+ /// You may dispose your font handle anytime, as long as it's not in use in . + /// Font handles may be constructed anytime, as long as the owner or + /// is not disposed.
+ ///
+ /// If you were storing , consider if the job can be achieved solely by using + /// without directly using an instance of .
+ /// If you do need it, evaluate if you need to access fonts outside the main thread.
+ /// If it is the case, use to obtain a safe-to-access instance of + /// , once resolves.
+ /// Otherwise, use , and obtain the instance of via + /// . Do not let the escape the using scope.
+ ///
+ /// If your plugin sets to a non-default value, then + /// should be accessed using + /// , as the font handle member variables are only available + /// once drawing facilities are available.
+ ///
+ /// Examples:
+ /// * .
+ /// * .
+ /// * ctor.
+ /// * : + /// note how a new instance of is constructed, and + /// is called from another function, without having to manually + /// initialize font rebuild process. /// [Obsolete("See remarks.", false)] [Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)] @@ -120,18 +150,11 @@ public sealed class UiBuilder : IDisposable /// /// Gets or sets an action that is called any time right after ImGui fonts are rebuilt.
- /// Any ImFontPtr objects that you store can be invalidated when fonts are rebuilt + /// Any ImFontPtr objects that you store can be invalidated when fonts are rebuilt /// (at any time), so you should both reload your custom fonts and restore those /// pointers inside this handler. ///
- /// - /// To add your custom font, use . or - /// .
- /// To be notified on font changes after fonts are built, use - /// ..
- /// For all other purposes, use .. - ///
- [Obsolete($"Use {nameof(this.FontAtlas)} instead.", false)] + [Obsolete($"See remarks for {nameof(BuildFonts)}.", false)] [Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)] public event Action? AfterBuildFonts; From 127b91f4b0d05ca08591f3b8cfbda8a6d9f706ea Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Sun, 21 Jan 2024 04:12:40 +0900 Subject: [PATCH 4/9] Fix doc --- Dalamud/Interface/UiBuilder.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Dalamud/Interface/UiBuilder.cs b/Dalamud/Interface/UiBuilder.cs index 02decf103..c27c9ab84 100644 --- a/Dalamud/Interface/UiBuilder.cs +++ b/Dalamud/Interface/UiBuilder.cs @@ -140,9 +140,9 @@ public sealed class UiBuilder : IDisposable /// * .
/// * ctor.
/// * : - /// note how a new instance of is constructed, and - /// is called from another function, without having to manually - /// initialize font rebuild process. + /// note how the construction of a new instance of and + /// call of are done in different functions, + /// without having to manually initiate font rebuild process. /// [Obsolete("See remarks.", false)] [Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)] From af1133f99973af30d01b1946f7513810320a6243 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Sun, 21 Jan 2024 04:21:26 +0900 Subject: [PATCH 5/9] Determine optional assets availability on startup --- Dalamud.CorePlugin/PluginImpl.cs | 4 ++++ Dalamud/Storage/Assets/DalamudAssetManager.cs | 8 ++++++++ 2 files changed, 12 insertions(+) diff --git a/Dalamud.CorePlugin/PluginImpl.cs b/Dalamud.CorePlugin/PluginImpl.cs index ef99f6def..96d212dd3 100644 --- a/Dalamud.CorePlugin/PluginImpl.cs +++ b/Dalamud.CorePlugin/PluginImpl.cs @@ -69,6 +69,10 @@ namespace Dalamud.CorePlugin this.Interface.UiBuilder.Draw += this.OnDraw; this.Interface.UiBuilder.OpenConfigUi += this.OnOpenConfigUi; this.Interface.UiBuilder.OpenMainUi += this.OnOpenMainUi; + this.Interface.UiBuilder.DefaultFontHandle.ImFontChanged += fc => + { + Log.Information($"CorePlugin : DefaultFontHandle.ImFontChanged called {fc}"); + }; Service.Get().AddHandler("/coreplug", new(this.OnCommand) { HelpMessage = "Access the plugin." }); diff --git a/Dalamud/Storage/Assets/DalamudAssetManager.cs b/Dalamud/Storage/Assets/DalamudAssetManager.cs index 70a91c4bf..7edb1c61d 100644 --- a/Dalamud/Storage/Assets/DalamudAssetManager.cs +++ b/Dalamud/Storage/Assets/DalamudAssetManager.cs @@ -69,6 +69,14 @@ internal sealed class DalamudAssetManager : IServiceType, IDisposable, IDalamudA .Select(x => x.ToContentDisposedTask())) .ContinueWith(_ => loadTimings.Dispose()), "Prevent Dalamud from loading more stuff, until we've ensured that all required assets are available."); + + Task.WhenAll( + Enum.GetValues() + .Where(x => x is not DalamudAsset.Empty4X4) + .Where(x => x.GetAttribute()?.Required is false) + .Select(this.CreateStreamAsync) + .Select(x => x.ToContentDisposedTask())) + .ContinueWith(r => Log.Verbose($"Optional assets load state: {r}")); } /// From 3e3297f7a8eeb3edcd83021ac90c25fb1d3f0482 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Sun, 21 Jan 2024 04:49:51 +0900 Subject: [PATCH 6/9] Use Lock instead of .ImFont --- Dalamud/Interface/Internal/InterfaceManager.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Dalamud/Interface/Internal/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs index 62f9145bf..159ae15bf 100644 --- a/Dalamud/Interface/Internal/InterfaceManager.cs +++ b/Dalamud/Interface/Internal/InterfaceManager.cs @@ -725,13 +725,16 @@ internal class InterfaceManager : IDisposable, IServiceType // Do not use DefaultFont, IconFont, and MonoFont. // Use font handles directly. + using var defaultFont = this.DefaultFontHandle.Lock(); + using var monoFont = this.MonoFontHandle.Lock(); + // Fill missing glyphs in MonoFont from DefaultFont - tk.CopyGlyphsAcrossFonts(this.DefaultFontHandle.ImFont, this.MonoFontHandle.ImFont, true); + tk.CopyGlyphsAcrossFonts(defaultFont, monoFont, true); // Update default font unsafe { - ImGui.GetIO().NativePtr->FontDefault = this.DefaultFontHandle.ImFont; + ImGui.GetIO().NativePtr->FontDefault = defaultFont; } // Broadcast to auto-rebuilding instances From a409ea60d6442a37c218dee954e27fc4e5e113d9 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Sun, 21 Jan 2024 04:54:35 +0900 Subject: [PATCH 7/9] Update docs --- .../IFontAtlasBuildToolkitPreBuild.cs | 14 ++++++++------ Dalamud/Interface/ManagedFontAtlas/IFontHandle.cs | 3 ++- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPreBuild.cs b/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPreBuild.cs index cb8a27a54..38d8d2fe8 100644 --- a/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPreBuild.cs +++ b/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPreBuild.cs @@ -54,11 +54,11 @@ public interface IFontAtlasBuildToolkitPreBuild : IFontAtlasBuildToolkit /// /// Adds a font from memory region allocated using .
- /// It WILL crash if you try to use a memory pointer allocated in some other way.
- /// + /// 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.. @@ -81,9 +81,11 @@ public interface IFontAtlasBuildToolkitPreBuild : IFontAtlasBuildToolkit /// /// 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. + /// 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.. diff --git a/Dalamud/Interface/ManagedFontAtlas/IFontHandle.cs b/Dalamud/Interface/ManagedFontAtlas/IFontHandle.cs index eb57b815f..877cd60c9 100644 --- a/Dalamud/Interface/ManagedFontAtlas/IFontHandle.cs +++ b/Dalamud/Interface/ManagedFontAtlas/IFontHandle.cs @@ -53,7 +53,8 @@ public interface IFontHandle : IDisposable /// /// Locks the fully constructed instance of corresponding to the this - /// , for read-only use in any thread. + /// , for use in any thread.
+ /// Modification of the font will exhibit undefined behavior if some other thread also uses the font. ///
/// An instance of that must be disposed after use. /// From 29b3e0aa97683d1dcb11421fd3aef0b909b05119 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Sun, 21 Jan 2024 13:15:36 +0900 Subject: [PATCH 8/9] Make IFontHandle.Push return IDisposable, and add IFontHandle.Pop --- Dalamud/Dalamud.csproj | 1 + Dalamud/Interface/GameFonts/GameFontHandle.cs | 4 +- .../Interface/Internal/InterfaceManager.cs | 7 ++ .../Widgets/GamePrebakedFontsTestWidget.cs | 20 ++++- .../Interface/ManagedFontAtlas/IFontHandle.cs | 49 +++--------- .../Internals/DelegateFontHandle.cs | 36 ++++++++- .../Internals/GamePrebakedFontHandle.cs | 33 +++++++- .../Internals/SimplePushedFont.cs | 78 +++++++++++++++++++ Dalamud/Interface/UiBuilder.cs | 4 +- 9 files changed, 185 insertions(+), 47 deletions(-) create mode 100644 Dalamud/Interface/ManagedFontAtlas/Internals/SimplePushedFont.cs diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj index ba044a555..f58a0c47a 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -70,6 +70,7 @@ + all diff --git a/Dalamud/Interface/GameFonts/GameFontHandle.cs b/Dalamud/Interface/GameFonts/GameFontHandle.cs index 6591ce0fe..7bda27eae 100644 --- a/Dalamud/Interface/GameFonts/GameFontHandle.cs +++ b/Dalamud/Interface/GameFonts/GameFontHandle.cs @@ -72,8 +72,8 @@ public sealed class GameFontHandle : IFontHandle /// An that can be used to pop the font on dispose. public IDisposable Push() => this.fontHandle.Push(); - /// - IFontHandle.FontPopper IFontHandle.Push() => this.fontHandle.Push(); + /// + public void Pop() => this.fontHandle.Pop(); /// public Task WaitAsync() => this.fontHandle.WaitAsync(); diff --git a/Dalamud/Interface/Internal/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs index 159ae15bf..e1b714ee8 100644 --- a/Dalamud/Interface/Internal/InterfaceManager.cs +++ b/Dalamud/Interface/Internal/InterfaceManager.cs @@ -230,6 +230,11 @@ internal class InterfaceManager : IDisposable, IServiceType ///
public Task FontBuildTask => WhenFontsReady().dalamudAtlas!.BuildTask; + /// + /// Gets the number of calls to so far. + /// + public long CumulativePresentCalls { get; private set; } + /// /// Dispose of managed and unmanaged resources. /// @@ -647,6 +652,8 @@ internal class InterfaceManager : IDisposable, IServiceType */ private IntPtr PresentDetour(IntPtr swapChain, uint syncInterval, uint presentFlags) { + this.CumulativePresentCalls++; + 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"); diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/GamePrebakedFontsTestWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/GamePrebakedFontsTestWidget.cs index b3b57343c..7b649a895 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/GamePrebakedFontsTestWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/GamePrebakedFontsTestWidget.cs @@ -163,6 +163,7 @@ internal class GamePrebakedFontsTestWidget : IDataWindowWidget, IDisposable .ToArray()); var offsetX = ImGui.CalcTextSize("99.9pt").X + (ImGui.GetStyle().FramePadding.X * 2); + var counter = 0; foreach (var (family, items) in this.fontHandles) { if (!ImGui.CollapsingHeader($"{family} Family")) @@ -188,10 +189,21 @@ internal class GamePrebakedFontsTestWidget : IDataWindowWidget, IDisposable { 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); + if (counter++ % 2 == 0) + { + using var pushPop = handle.Value.Push(); + ImGuiNative.igTextUnformatted( + this.testStringBuffer.Data, + this.testStringBuffer.Data + this.testStringBuffer.Length); + } + else + { + handle.Value.Push(); + ImGuiNative.igTextUnformatted( + this.testStringBuffer.Data, + this.testStringBuffer.Data + this.testStringBuffer.Length); + handle.Value.Pop(); + } } } finally diff --git a/Dalamud/Interface/ManagedFontAtlas/IFontHandle.cs b/Dalamud/Interface/ManagedFontAtlas/IFontHandle.cs index 877cd60c9..94edc9777 100644 --- a/Dalamud/Interface/ManagedFontAtlas/IFontHandle.cs +++ b/Dalamud/Interface/ManagedFontAtlas/IFontHandle.cs @@ -65,17 +65,23 @@ public interface IFontHandle : IDisposable ImFontLocked Lock(); /// - /// Pushes the current font into ImGui font stack using , if available.
+ /// Pushes the current font into ImGui font stack, 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. + /// A disposable object that will pop the font on dispose. /// If called outside of the main thread. /// - /// Only intended for use with using keywords, such as using (handle.Push()).
- /// Should you store or transfer the return value to somewhere else, use as the type. + /// This function uses , and may do extra things. + /// Use or to undo this operation. + /// Do not use . ///
- FontPopper Push(); + IDisposable Push(); + + /// + /// Pops the font pushed to ImGui using , cleaning up any extra information as needed. + /// + void Pop(); /// /// Waits for to become true. @@ -124,37 +130,4 @@ public interface IFontHandle : IDisposable this.ImFont = default; } } - - /// - /// The wrapper for popping fonts. - /// - public struct FontPopper : IDisposable - { - private int count; - - /// - /// Initializes a new instance of the struct. - /// - /// The font to push. - /// Whether to push. - internal FontPopper(ImFontPtr fontPtr, bool push) - { - if (!push) - return; - - ThreadSafety.AssertMainThread(); - - this.count = 1; - ImGui.PushFont(fontPtr); - } - - /// - public void Dispose() - { - ThreadSafety.AssertMainThread(); - - while (this.count-- > 0) - ImGui.PopFont(); - } - } } diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/DelegateFontHandle.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/DelegateFontHandle.cs index f50967fae..e1c18e923 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/DelegateFontHandle.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/DelegateFontHandle.cs @@ -2,12 +2,15 @@ using System.Linq; using System.Threading.Tasks; +using Dalamud.Interface.Internal; using Dalamud.Interface.Utility; using Dalamud.Logging.Internal; using Dalamud.Utility; using ImGuiNET; +using Serilog; + namespace Dalamud.Interface.ManagedFontAtlas.Internals; /// @@ -15,7 +18,10 @@ namespace Dalamud.Interface.ManagedFontAtlas.Internals; /// internal class DelegateFontHandle : IFontHandle.IInternal { + private readonly List pushedFonts = new(8); + private IFontHandleManager? manager; + private long lastCumulativePresentCalls; /// /// Initializes a new instance of the class. @@ -53,6 +59,8 @@ internal class DelegateFontHandle : IFontHandle.IInternal /// public void Dispose() { + if (this.pushedFonts.Count > 0) + Log.Warning($"{nameof(IFontHandle)}.{nameof(IDisposable.Dispose)}: fonts were still in a stack."); this.manager?.FreeFontHandle(this); this.manager = null; this.Disposed?.InvokeSafely(this); @@ -96,7 +104,33 @@ internal class DelegateFontHandle : IFontHandle.IInternal } /// - public IFontHandle.FontPopper Push() => new(this.ImFont, this.Available); + public IDisposable Push() + { + ThreadSafety.AssertMainThread(); + var cumulativePresentCalls = Service.GetNullable()?.CumulativePresentCalls ?? 0L; + if (this.lastCumulativePresentCalls != cumulativePresentCalls) + { + this.lastCumulativePresentCalls = cumulativePresentCalls; + if (this.pushedFonts.Count > 0) + { + Log.Warning( + $"{nameof(this.Push)} has been called, but the handle-private stack was not empty. " + + $"You might be missing a call to {nameof(this.Pop)}."); + this.pushedFonts.Clear(); + } + } + + var rented = SimplePushedFont.Rent(this.pushedFonts, this.ImFont, this.Available); + this.pushedFonts.Add(rented); + return rented; + } + + /// + public void Pop() + { + ThreadSafety.AssertMainThread(); + this.pushedFonts[^1].Dispose(); + } /// public Task WaitAsync() diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/GamePrebakedFontHandle.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/GamePrebakedFontHandle.cs index c05b3a96d..0e8301785 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/GamePrebakedFontHandle.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/GamePrebakedFontHandle.cs @@ -16,6 +16,8 @@ using ImGuiNET; using Lumina.Data.Files; +using Serilog; + using Vector4 = System.Numerics.Vector4; namespace Dalamud.Interface.ManagedFontAtlas.Internals; @@ -35,7 +37,10 @@ internal class GamePrebakedFontHandle : IFontHandle.IInternal /// public static readonly char SeIconCharMax = (char)Enum.GetValues().Max(); + private readonly List pushedFonts = new(8); + private IFontHandleManager? manager; + private long lastCumulativePresentCalls; /// /// Initializes a new instance of the class. @@ -160,7 +165,33 @@ internal class GamePrebakedFontHandle : IFontHandle.IInternal } /// - public IFontHandle.FontPopper Push() => new(this.ImFont, this.Available); + public IDisposable Push() + { + ThreadSafety.AssertMainThread(); + var cumulativePresentCalls = Service.GetNullable()?.CumulativePresentCalls ?? 0L; + if (this.lastCumulativePresentCalls != cumulativePresentCalls) + { + this.lastCumulativePresentCalls = cumulativePresentCalls; + if (this.pushedFonts.Count > 0) + { + Log.Warning( + $"{nameof(this.Push)} has been called, but the handle-private stack was not empty. " + + $"You might be missing a call to {nameof(this.Pop)}."); + this.pushedFonts.Clear(); + } + } + + var rented = SimplePushedFont.Rent(this.pushedFonts, this.ImFont, this.Available); + this.pushedFonts.Add(rented); + return rented; + } + + /// + public void Pop() + { + ThreadSafety.AssertMainThread(); + this.pushedFonts[^1].Dispose(); + } /// public Task WaitAsync() diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/SimplePushedFont.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/SimplePushedFont.cs new file mode 100644 index 000000000..3f7255386 --- /dev/null +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/SimplePushedFont.cs @@ -0,0 +1,78 @@ +using System.Collections.Generic; +using System.Diagnostics; + +using Dalamud.Interface.Utility; + +using ImGuiNET; + +using Microsoft.Extensions.ObjectPool; + +using Serilog; + +namespace Dalamud.Interface.ManagedFontAtlas.Internals; + +/// +/// Reusable font push/popper. +/// +internal sealed class SimplePushedFont : IDisposable +{ + // Using constructor instead of DefaultObjectPoolProvider, since we do not want the pool to call Dispose. + private static readonly ObjectPool Pool = + new DefaultObjectPool(new DefaultPooledObjectPolicy()); + + private List? stack; + private ImFontPtr font; + + /// + /// Pushes the font, and return an instance of . + /// + /// The -private stack. + /// The font pointer being pushed. + /// Whether to push. + /// this. + public static SimplePushedFont Rent(List stack, ImFontPtr fontPtr, bool push) + { + push &= !fontPtr.IsNull(); + + var rented = Pool.Get(); + Debug.Assert(rented.font.IsNull(), "Rented object must not have its font set"); + rented.stack = stack; + + if (push) + { + rented.font = fontPtr; + ImGui.PushFont(fontPtr); + } + + return rented; + } + + /// + public unsafe void Dispose() + { + if (this.stack is null || !ReferenceEquals(this.stack[^1], this)) + { + throw new InvalidOperationException("Tried to pop a non-pushed font."); + } + + this.stack.RemoveAt(this.stack.Count - 1); + + if (!this.font.IsNull()) + { + if (ImGui.GetFont().NativePtr == this.font.NativePtr) + { + ImGui.PopFont(); + } + else + { + Log.Warning( + $"{nameof(IFontHandle.Pop)}: The font currently being popped does not match the pushed font. " + + $"Doing nothing."); + } + } + + this.font = default; + this.stack = null; + Pool.Return(this); + } +} diff --git a/Dalamud/Interface/UiBuilder.cs b/Dalamud/Interface/UiBuilder.cs index c27c9ab84..1134704ee 100644 --- a/Dalamud/Interface/UiBuilder.cs +++ b/Dalamud/Interface/UiBuilder.cs @@ -762,9 +762,11 @@ public sealed class UiBuilder : IDisposable public IFontHandle.ImFontLocked Lock() => this.wrapped?.Lock() ?? throw new ObjectDisposedException(nameof(FontHandleWrapper)); - public IFontHandle.FontPopper Push() => + public IDisposable Push() => this.wrapped?.Push() ?? throw new ObjectDisposedException(nameof(FontHandleWrapper)); + public void Pop() => this.wrapped?.Pop(); + public Task WaitAsync() => this.wrapped?.WaitAsync().ContinueWith(_ => (IFontHandle)this) ?? throw new ObjectDisposedException(nameof(FontHandleWrapper)); From fc4d08927b82f332b81f318091226b06cd0a993c Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Sun, 21 Jan 2024 15:11:31 +0900 Subject: [PATCH 9/9] Fix Dalamud Configuration revert not rebuilding fonts --- Dalamud/Interface/Internal/Windows/Settings/SettingsWindow.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Dalamud/Interface/Internal/Windows/Settings/SettingsWindow.cs b/Dalamud/Interface/Internal/Windows/Settings/SettingsWindow.cs index 027e1a571..c325028e1 100644 --- a/Dalamud/Interface/Internal/Windows/Settings/SettingsWindow.cs +++ b/Dalamud/Interface/Internal/Windows/Settings/SettingsWindow.cs @@ -69,6 +69,7 @@ internal class SettingsWindow : Window var fontAtlasFactory = Service.Get(); var rebuildFont = fontAtlasFactory.UseAxis != configuration.UseAxisFontsFromGame; + rebuildFont |= !Equals(ImGui.GetIO().FontGlobalScale, configuration.GlobalUiScale); ImGui.GetIO().FontGlobalScale = configuration.GlobalUiScale; fontAtlasFactory.UseAxisOverride = null;