From 29b3e0aa97683d1dcb11421fd3aef0b909b05119 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Sun, 21 Jan 2024 13:15:36 +0900 Subject: [PATCH] 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));