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));