Make IFontHandle.Push return IDisposable, and add IFontHandle.Pop

This commit is contained in:
Soreepeong 2024-01-21 13:15:36 +09:00
parent a409ea60d6
commit 29b3e0aa97
9 changed files with 185 additions and 47 deletions

View file

@ -70,6 +70,7 @@
<PackageReference Include="JetBrains.Annotations" Version="2021.2.0" />
<PackageReference Include="Lumina" Version="3.15.2" />
<PackageReference Include="Lumina.Excel" Version="6.5.2" />
<PackageReference Include="Microsoft.Extensions.ObjectPool" Version="8.0.1" />
<PackageReference Include="Microsoft.Windows.CsWin32" Version="0.3.46-beta">
<PrivateAssets>all</PrivateAssets>
</PackageReference>

View file

@ -73,7 +73,7 @@ public sealed class GameFontHandle : IFontHandle
public IDisposable Push() => this.fontHandle.Push();
/// <inheritdoc />
IFontHandle.FontPopper IFontHandle.Push() => this.fontHandle.Push();
public void Pop() => this.fontHandle.Pop();
/// <inheritdoc />
public Task<IFontHandle> WaitAsync() => this.fontHandle.WaitAsync();

View file

@ -230,6 +230,11 @@ internal class InterfaceManager : IDisposable, IServiceType
/// </summary>
public Task FontBuildTask => WhenFontsReady().dalamudAtlas!.BuildTask;
/// <summary>
/// Gets the number of calls to <see cref="PresentDetour"/> so far.
/// </summary>
public long CumulativePresentCalls { get; private set; }
/// <summary>
/// Dispose of managed and unmanaged resources.
/// </summary>
@ -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");

View file

@ -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,11 +189,22 @@ internal class GamePrebakedFontsTestWidget : IDataWindowWidget, IDisposable
{
if (!this.useGlobalScale)
ImGuiNative.igSetWindowFontScale(1 / ImGuiHelpers.GlobalScale);
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
{

View file

@ -65,17 +65,23 @@ public interface IFontHandle : IDisposable
ImFontLocked Lock();
/// <summary>
/// Pushes the current font into ImGui font stack using <see cref="ImGui.PushFont"/>, if available.<br />
/// Pushes the current font into ImGui font stack, if available.<br />
/// Use <see cref="ImGui.GetFont"/> to access the current font.<br />
/// You may not access the font once you dispose this object.
/// </summary>
/// <returns>A disposable object that will call <see cref="ImGui.PopFont"/>(1) on dispose.</returns>
/// <returns>A disposable object that will pop the font on dispose.</returns>
/// <exception cref="InvalidOperationException">If called outside of the main thread.</exception>
/// <remarks>
/// Only intended for use with <c>using</c> keywords, such as <c>using (handle.Push())</c>.<br />
/// Should you store or transfer the return value to somewhere else, use <see cref="IDisposable"/> as the type.
/// This function uses <see cref="ImGui.PushFont"/>, and may do extra things.
/// Use <see cref="IDisposable.Dispose"/> or <see cref="Pop"/> to undo this operation.
/// Do not use <see cref="ImGui.PopFont"/>.
/// </remarks>
FontPopper Push();
IDisposable Push();
/// <summary>
/// Pops the font pushed to ImGui using <see cref="Push"/>, cleaning up any extra information as needed.
/// </summary>
void Pop();
/// <summary>
/// Waits for <see cref="Available"/> to become <c>true</c>.
@ -124,37 +130,4 @@ public interface IFontHandle : IDisposable
this.ImFont = default;
}
}
/// <summary>
/// The wrapper for popping fonts.
/// </summary>
public struct FontPopper : IDisposable
{
private int count;
/// <summary>
/// Initializes a new instance of the <see cref="FontPopper"/> struct.
/// </summary>
/// <param name="fontPtr">The font to push.</param>
/// <param name="push">Whether to push.</param>
internal FontPopper(ImFontPtr fontPtr, bool push)
{
if (!push)
return;
ThreadSafety.AssertMainThread();
this.count = 1;
ImGui.PushFont(fontPtr);
}
/// <inheritdoc />
public void Dispose()
{
ThreadSafety.AssertMainThread();
while (this.count-- > 0)
ImGui.PopFont();
}
}
}

View file

@ -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;
/// <summary>
@ -15,7 +18,10 @@ namespace Dalamud.Interface.ManagedFontAtlas.Internals;
/// </summary>
internal class DelegateFontHandle : IFontHandle.IInternal
{
private readonly List<IDisposable> pushedFonts = new(8);
private IFontHandleManager? manager;
private long lastCumulativePresentCalls;
/// <summary>
/// Initializes a new instance of the <see cref="DelegateFontHandle"/> class.
@ -53,6 +59,8 @@ internal class DelegateFontHandle : IFontHandle.IInternal
/// <inheritdoc/>
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
}
/// <inheritdoc/>
public IFontHandle.FontPopper Push() => new(this.ImFont, this.Available);
public IDisposable Push()
{
ThreadSafety.AssertMainThread();
var cumulativePresentCalls = Service<InterfaceManager>.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;
}
/// <inheritdoc/>
public void Pop()
{
ThreadSafety.AssertMainThread();
this.pushedFonts[^1].Dispose();
}
/// <inheritdoc/>
public Task<IFontHandle> WaitAsync()

View file

@ -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
/// </summary>
public static readonly char SeIconCharMax = (char)Enum.GetValues<SeIconChar>().Max();
private readonly List<IDisposable> pushedFonts = new(8);
private IFontHandleManager? manager;
private long lastCumulativePresentCalls;
/// <summary>
/// Initializes a new instance of the <see cref="GamePrebakedFontHandle"/> class.
@ -160,7 +165,33 @@ internal class GamePrebakedFontHandle : IFontHandle.IInternal
}
/// <inheritdoc/>
public IFontHandle.FontPopper Push() => new(this.ImFont, this.Available);
public IDisposable Push()
{
ThreadSafety.AssertMainThread();
var cumulativePresentCalls = Service<InterfaceManager>.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;
}
/// <inheritdoc/>
public void Pop()
{
ThreadSafety.AssertMainThread();
this.pushedFonts[^1].Dispose();
}
/// <inheritdoc/>
public Task<IFontHandle> WaitAsync()

View file

@ -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;
/// <summary>
/// Reusable font push/popper.
/// </summary>
internal sealed class SimplePushedFont : IDisposable
{
// Using constructor instead of DefaultObjectPoolProvider, since we do not want the pool to call Dispose.
private static readonly ObjectPool<SimplePushedFont> Pool =
new DefaultObjectPool<SimplePushedFont>(new DefaultPooledObjectPolicy<SimplePushedFont>());
private List<IDisposable>? stack;
private ImFontPtr font;
/// <summary>
/// Pushes the font, and return an instance of <see cref="SimplePushedFont"/>.
/// </summary>
/// <param name="stack">The <see cref="IFontHandle"/>-private stack.</param>
/// <param name="fontPtr">The font pointer being pushed.</param>
/// <param name="push">Whether to push.</param>
/// <returns><c>this</c>.</returns>
public static SimplePushedFont Rent(List<IDisposable> 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;
}
/// <inheritdoc />
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);
}
}

View file

@ -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<IFontHandle> WaitAsync() =>
this.wrapped?.WaitAsync().ContinueWith(_ => (IFontHandle)this) ??
throw new ObjectDisposedException(nameof(FontHandleWrapper));