Lock font resources on Push and miscellaneous direct accesses

These changes ensure that using a font under some other thread's ownership from the UI thread for rendering into ImGui purposes always work.

* `FontHandle`:
    * Moved common code from `DelegateFontHandle` and `GamePrebakedFontHandle`.
    * Added `LockUntilPostFrame` so that the obtained `ImFontPtr` and its accompanying resources are kept valid until everything is rendered.
    * Added more code comments to `Try/Lock`.
    * Moved font access thread checking logic from `InterfaceManager` to `LockUntilPostFrame`.
    * `Push`ing a font will now also perform `LockUntilPostFrame`.
* `GameFontHandle`: Make the property `ImFont` a forwarder to `FontHandle.LockUntilPostFrame`.
* `InterfaceManager`:
    * Added companion logic to `FontHandle.LockUntilPostFrame`.
    * Accessing default/icon/mono fonts will forward to `FontHandle.LockUntilPostFrame`.
    * Changed `List<T>` to `ConcurrentBag<T>` as texture disposal can be done outside the main thread, and a race condition is possible.
This commit is contained in:
Soreepeong 2024-01-23 20:51:29 +09:00
parent e20daed848
commit 5479149e79
8 changed files with 331 additions and 347 deletions

View file

@ -15,15 +15,16 @@ namespace Dalamud.Interface.GameFonts;
[Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)]
public sealed class GameFontHandle : IFontHandle
{
private readonly IFontHandle.IInternal fontHandle;
private readonly GamePrebakedFontHandle fontHandle;
private readonly FontAtlasFactory fontAtlasFactory;
/// <summary>
/// Initializes a new instance of the <see cref="GameFontHandle"/> class.
/// Initializes a new instance of the <see cref="GameFontHandle"/> class.<br />
/// Ownership of <paramref name="fontHandle"/> is transferred.
/// </summary>
/// <param name="fontHandle">The wrapped <see cref="IFontHandle"/>.</param>
/// <param name="fontHandle">The wrapped <see cref="GamePrebakedFontHandle"/>.</param>
/// <param name="fontAtlasFactory">An instance of <see cref="FontAtlasFactory"/>.</param>
internal GameFontHandle(IFontHandle.IInternal fontHandle, FontAtlasFactory fontAtlasFactory)
internal GameFontHandle(GamePrebakedFontHandle fontHandle, FontAtlasFactory fontAtlasFactory)
{
this.fontHandle = fontHandle;
this.fontAtlasFactory = fontAtlasFactory;
@ -42,9 +43,15 @@ public sealed class GameFontHandle : IFontHandle
/// <inheritdoc />
public bool Available => this.fontHandle.Available;
/// <inheritdoc cref="IFontHandle.IInternal.ImFont"/>
[Obsolete($"Use {nameof(Push)}, and then use {nameof(ImGui.GetFont)} instead.", false)]
public ImFontPtr ImFont => this.fontHandle.ImFont;
/// <summary>
/// Gets the font.<br />
/// Use of this properly is safe only from the UI thread.<br />
/// Use <see cref="IFontHandle.Push"/> if the intended purpose of this property is <see cref="ImGui.PushFont"/>.<br />
/// Futures changes may make simple <see cref="ImGui.PushFont"/> not enough.<br />
/// If you need to access a font outside the UI thread, use <see cref="IFontHandle.Lock"/>.
/// </summary>
[Obsolete($"Use {nameof(Push)}-{nameof(ImGui.GetFont)} or {nameof(Lock)} instead.", false)]
public ImFontPtr ImFont => this.fontHandle.LockUntilPostFrame();
/// <summary>
/// Gets the font style. Only applicable for <see cref="GameFontHandle"/>.
@ -66,10 +73,7 @@ public sealed class GameFontHandle : IFontHandle
/// <inheritdoc />
public IFontHandle.ImFontLocked Lock() => this.fontHandle.Lock();
/// <summary>
/// Pushes the font.
/// </summary>
/// <returns>An <see cref="IDisposable"/> that can be used to pop the font on dispose.</returns>
/// <inheritdoc />
public IDisposable Push() => this.fontHandle.Push();
/// <inheritdoc />

View file

@ -1,3 +1,4 @@
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
@ -20,8 +21,6 @@ using Dalamud.Interface.ManagedFontAtlas.Internals;
using Dalamud.Interface.Style;
using Dalamud.Interface.Utility;
using Dalamud.Interface.Windowing;
using Dalamud.Plugin.Internal;
using Dalamud.Plugin.Internal.Types;
using Dalamud.Utility;
using Dalamud.Utility.Timing;
using ImGuiNET;
@ -63,15 +62,9 @@ internal class InterfaceManager : IDisposable, IServiceType
/// </summary>
public const float DefaultFontSizePx = (DefaultFontSizePt * 4.0f) / 3.0f;
private const int NonMainThreadFontAccessWarningCheckInterval = 10000;
private static readonly ConditionalWeakTable<LocalPlugin, object> NonMainThreadFontAccessWarning = new();
private static long nextNonMainThreadFontAccessWarningCheck;
private readonly ConcurrentBag<DalamudTextureWrap> deferredDisposeTextures = new();
private readonly ConcurrentBag<IFontHandle.ImFontLocked> deferredDisposeImFontLockeds = new();
private readonly List<DalamudTextureWrap> deferredDisposeTextures = new();
[ServiceManager.ServiceDependency]
private readonly Framework framework = Service<Framework>.Get();
[ServiceManager.ServiceDependency]
private readonly WndProcHookManager wndProcHookManager = Service<WndProcHookManager>.Get();
@ -127,34 +120,37 @@ internal class InterfaceManager : IDisposable, IServiceType
/// Gets the default ImGui font.<br />
/// <strong>Accessing this static property outside of the main thread is dangerous and not supported.</strong>
/// </summary>
public static ImFontPtr DefaultFont => WhenFontsReady().DefaultFontHandle!.ImFont.OrElse(ImGui.GetIO().FontDefault);
public static ImFontPtr DefaultFont =>
WhenFontsReady().DefaultFontHandle!.LockUntilPostFrame().OrElse(ImGui.GetIO().FontDefault);
/// <summary>
/// Gets an included FontAwesome icon font.<br />
/// <strong>Accessing this static property outside of the main thread is dangerous and not supported.</strong>
/// </summary>
public static ImFontPtr IconFont => WhenFontsReady().IconFontHandle!.ImFont.OrElse(ImGui.GetIO().FontDefault);
public static ImFontPtr IconFont =>
WhenFontsReady().IconFontHandle!.LockUntilPostFrame().OrElse(ImGui.GetIO().FontDefault);
/// <summary>
/// Gets an included monospaced font.<br />
/// <strong>Accessing this static property outside of the main thread is dangerous and not supported.</strong>
/// </summary>
public static ImFontPtr MonoFont => WhenFontsReady().MonoFontHandle!.ImFont.OrElse(ImGui.GetIO().FontDefault);
public static ImFontPtr MonoFont =>
WhenFontsReady().MonoFontHandle!.LockUntilPostFrame().OrElse(ImGui.GetIO().FontDefault);
/// <summary>
/// Gets the default font handle.
/// </summary>
public IFontHandle.IInternal? DefaultFontHandle { get; private set; }
public FontHandle? DefaultFontHandle { get; private set; }
/// <summary>
/// Gets the icon font handle.
/// </summary>
public IFontHandle.IInternal? IconFontHandle { get; private set; }
public FontHandle? IconFontHandle { get; private set; }
/// <summary>
/// Gets the mono font handle.
/// </summary>
public IFontHandle.IInternal? MonoFontHandle { get; private set; }
public FontHandle? MonoFontHandle { get; private set; }
/// <summary>
/// Gets or sets the pointer to ImGui.IO(), when it was last used.
@ -408,6 +404,15 @@ internal class InterfaceManager : IDisposable, IServiceType
this.deferredDisposeTextures.Add(wrap);
}
/// <summary>
/// Enqueue an <see cref="IFontHandle.ImFontLocked"/> to be disposed at the end of the frame.
/// </summary>
/// <param name="locked">The disposable.</param>
public void EnqueueDeferredDispose(in IFontHandle.ImFontLocked locked)
{
this.deferredDisposeImFontLockeds.Add(locked);
}
/// <summary>
/// Get video memory information.
/// </summary>
@ -466,29 +471,6 @@ internal class InterfaceManager : IDisposable, IServiceType
if (im?.dalamudAtlas is not { } atlas)
throw new InvalidOperationException($"Tried to access fonts before {nameof(ContinueConstruction)} call.");
if (!ThreadSafety.IsMainThread && nextNonMainThreadFontAccessWarningCheck < Environment.TickCount64)
{
nextNonMainThreadFontAccessWarningCheck =
Environment.TickCount64 + NonMainThreadFontAccessWarningCheckInterval;
var stack = new StackTrace();
if (Service<PluginManager>.GetNullable()?.FindCallingPlugin(stack) is { } plugin)
{
if (!NonMainThreadFontAccessWarning.TryGetValue(plugin, out _))
{
NonMainThreadFontAccessWarning.Add(plugin, new());
Log.Warning(
"[IM] {pluginName}: Accessing fonts outside the main thread is deprecated.\n{stack}",
plugin.Name,
stack);
}
}
else
{
// Dalamud internal should be made safe right now
throw new InvalidOperationException("Attempted to access fonts outside the main thread.");
}
}
if (!atlas.HasBuiltAtlas)
atlas.BuildTask.GetAwaiter().GetResult();
return im;
@ -673,28 +655,38 @@ internal class InterfaceManager : IDisposable, IServiceType
var pRes = this.presentHook!.Original(swapChain, syncInterval, presentFlags);
RenderImGui(this.scene!);
this.DisposeTextures();
this.CleanupPostImGuiRender();
return pRes;
}
RenderImGui(this.scene!);
this.DisposeTextures();
this.CleanupPostImGuiRender();
return this.presentHook!.Original(swapChain, syncInterval, presentFlags);
}
private void DisposeTextures()
private void CleanupPostImGuiRender()
{
if (this.deferredDisposeTextures.Count > 0)
if (!this.deferredDisposeTextures.IsEmpty)
{
Log.Verbose("[IM] Disposing {Count} textures", this.deferredDisposeTextures.Count);
foreach (var texture in this.deferredDisposeTextures)
var count = 0;
while (this.deferredDisposeTextures.TryTake(out var d))
{
texture.RealDispose();
count++;
d.RealDispose();
}
this.deferredDisposeTextures.Clear();
Log.Verbose("[IM] Disposing {Count} textures", count);
}
if (!this.deferredDisposeImFontLockeds.IsEmpty)
{
// Not logging; the main purpose of this is to keep resources used for rendering the frame to be kept
// referenced until the resources are actually done being used, and it is expected that this will be
// frequent.
while (this.deferredDisposeImFontLockeds.TryTake(out var d))
d.Dispose();
}
}
@ -709,9 +701,9 @@ internal class InterfaceManager : IDisposable, IServiceType
.CreateFontAtlas(nameof(InterfaceManager), FontAtlasAutoRebuildMode.Disable);
using (this.dalamudAtlas.SuppressAutoRebuild())
{
this.DefaultFontHandle = (IFontHandle.IInternal)this.dalamudAtlas.NewDelegateFontHandle(
this.DefaultFontHandle = (FontHandle)this.dalamudAtlas.NewDelegateFontHandle(
e => e.OnPreBuild(tk => tk.AddDalamudDefaultFont(DefaultFontSizePx)));
this.IconFontHandle = (IFontHandle.IInternal)this.dalamudAtlas.NewDelegateFontHandle(
this.IconFontHandle = (FontHandle)this.dalamudAtlas.NewDelegateFontHandle(
e => e.OnPreBuild(
tk => tk.AddFontAwesomeIconFont(
new()
@ -720,7 +712,7 @@ internal class InterfaceManager : IDisposable, IServiceType
GlyphMinAdvanceX = DefaultFontSizePx,
GlyphMaxAdvanceX = DefaultFontSizePx,
})));
this.MonoFontHandle = (IFontHandle.IInternal)this.dalamudAtlas.NewDelegateFontHandle(
this.MonoFontHandle = (FontHandle)this.dalamudAtlas.NewDelegateFontHandle(
e => e.OnPreBuild(
tk => tk.AddDalamudAssetFont(
DalamudAsset.InconsolataRegular,

View file

@ -21,21 +21,6 @@ public interface IFontHandle : IDisposable
/// </summary>
event Action<IFontHandle> ImFontChanged;
/// <summary>
/// Represents a reference counting handle for fonts. Dalamud internal use only.
/// </summary>
internal interface IInternal : IFontHandle
{
/// <summary>
/// Gets the font.<br />
/// Use of this properly is safe only from the UI thread.<br />
/// Use <see cref="IFontHandle.Push"/> if the intended purpose of this property is <see cref="ImGui.PushFont"/>.<br />
/// Futures changes may make simple <see cref="ImGui.PushFont"/> not enough.<br />
/// If you need to access a font outside the UI thread, consider using <see cref="IFontHandle.Lock"/>.
/// </summary>
ImFontPtr ImFont { get; }
}
/// <summary>
/// Gets the load exception, if it failed to load. Otherwise, it is null.
/// </summary>
@ -45,7 +30,6 @@ public interface IFontHandle : IDisposable
/// Gets a value indicating whether this font is ready for use.
/// </summary>
/// <remarks>
/// Once set to <c>true</c>, it will remain <c>true</c>.<br />
/// Use <see cref="Push"/> directly if you want to keep the current ImGui font if the font is not ready.<br />
/// Alternatively, use <see cref="WaitAsync"/> to wait for this property to become <c>true</c>.
/// </remarks>
@ -103,14 +87,13 @@ public interface IFontHandle : IDisposable
private IRefCountable? owner;
/// <summary>
/// Initializes a new instance of the <see cref="ImFontLocked"/> struct,
/// and incrase the reference count of <paramref name="owner"/>.
/// Initializes a new instance of the <see cref="ImFontLocked"/> struct.
/// Ownership of reference of <paramref name="owner"/> is transferred.
/// </summary>
/// <param name="imFont">The contained font.</param>
/// <param name="owner">The owner.</param>
internal ImFontLocked(ImFontPtr imFont, IRefCountable owner)
{
owner.AddRef();
this.ImFont = imFont;
this.owner = owner;
}

View file

@ -1,164 +1,35 @@
using System.Collections.Generic;
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>
/// A font handle representing a user-callback generated font.
/// </summary>
internal class DelegateFontHandle : IFontHandle.IInternal
internal sealed class DelegateFontHandle : FontHandle
{
private readonly List<IDisposable> pushedFonts = new(8);
private IFontHandleManager? manager;
private long lastCumulativePresentCalls;
/// <summary>
/// Initializes a new instance of the <see cref="DelegateFontHandle"/> class.
/// </summary>
/// <param name="manager">An instance of <see cref="IFontHandleManager"/>.</param>
/// <param name="callOnBuildStepChange">Callback for <see cref="IFontAtlas.BuildStepChange"/>.</param>
public DelegateFontHandle(IFontHandleManager manager, FontAtlasBuildStepDelegate callOnBuildStepChange)
: base(manager)
{
this.manager = manager;
this.CallOnBuildStepChange = callOnBuildStepChange;
}
/// <inheritdoc/>
public event Action<IFontHandle>? ImFontChanged;
private event Action<IFontHandle>? Disposed;
/// <summary>
/// Gets the function to be called on build step changes.
/// </summary>
public FontAtlasBuildStepDelegate CallOnBuildStepChange { get; }
/// <inheritdoc/>
public Exception? LoadException => this.ManagerNotDisposed.Substance?.GetBuildException(this);
/// <inheritdoc/>
public bool Available => this.ImFont.IsNotNullAndLoaded();
/// <inheritdoc/>
public ImFontPtr ImFont => this.ManagerNotDisposed.Substance?.GetFontPtr(this) ?? default;
private IFontHandleManager ManagerNotDisposed =>
this.manager ?? throw new ObjectDisposedException(nameof(GamePrebakedFontHandle));
/// <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);
this.ImFontChanged = null;
}
/// <inheritdoc/>
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();
}
}
}
/// <inheritdoc/>
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()
{
if (this.Available)
return Task.FromResult<IFontHandle>(this);
var tcs = new TaskCompletionSource<IFontHandle>();
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);
}
}
/// <summary>
/// Manager for <see cref="DelegateFontHandle"/>s.
/// </summary>
@ -216,7 +87,7 @@ internal class DelegateFontHandle : IFontHandle.IInternal
return;
foreach (var handle in hs.RelevantHandles)
handle.ImFontChanged?.InvokeSafely(handle);
handle.InvokeImFontChanged();
}
/// <inheritdoc/>

View file

@ -0,0 +1,263 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using Dalamud.Interface.Internal;
using Dalamud.Interface.Utility;
using Dalamud.Plugin.Internal;
using Dalamud.Plugin.Internal.Types;
using Dalamud.Utility;
using ImGuiNET;
using Serilog;
namespace Dalamud.Interface.ManagedFontAtlas.Internals;
/// <summary>
/// Default implementation for <see cref="FontHandle"/>.
/// </summary>
internal abstract class FontHandle : IFontHandle
{
private const int NonMainThreadFontAccessWarningCheckInterval = 10000;
private static readonly ConditionalWeakTable<LocalPlugin, object> NonMainThreadFontAccessWarning = new();
private static long nextNonMainThreadFontAccessWarningCheck;
private readonly InterfaceManager interfaceManager;
private readonly List<IDisposable> pushedFonts = new(8);
private IFontHandleManager? manager;
private long lastCumulativePresentCalls;
/// <summary>
/// Initializes a new instance of the <see cref="FontHandle"/> class.
/// </summary>
/// <param name="manager">An instance of <see cref="IFontHandleManager"/>.</param>
protected FontHandle(IFontHandleManager manager)
{
this.interfaceManager = Service<InterfaceManager>.Get();
this.manager = manager;
}
/// <inheritdoc/>
public event Action<IFontHandle>? ImFontChanged;
/// <summary>
/// Event to be called on the first <see cref="IDisposable.Dispose"/> call.
/// </summary>
protected event Action<IFontHandle>? Disposed;
/// <inheritdoc/>
public Exception? LoadException => this.Manager.Substance?.GetBuildException(this);
/// <inheritdoc/>
public bool Available => (this.Manager.Substance?.GetFontPtr(this) ?? default).IsNotNullAndLoaded();
/// <summary>
/// Gets the associated <see cref="IFontHandleManager"/>.
/// </summary>
/// <exception cref="ObjectDisposedException">When the object has already been disposed.</exception>
protected IFontHandleManager Manager => this.manager ?? throw new ObjectDisposedException(this.GetType().Name);
/// <inheritdoc/>
public void Dispose()
{
if (this.manager is null)
return;
this.Dispose(true);
GC.SuppressFinalize(this);
}
/// <summary>
/// Obtains an instance of <see cref="ImFontPtr"/> corresponding to this font handle,
/// to be released after rendering the current frame.
/// </summary>
/// <returns>The font pointer, or default if unavailble.</returns>
/// <remarks>
/// Behavior is undefined on access outside the main thread.
/// </remarks>
public ImFontPtr LockUntilPostFrame()
{
if (this.TryLock(out _) is not { } locked)
return default;
if (!ThreadSafety.IsMainThread && nextNonMainThreadFontAccessWarningCheck < Environment.TickCount64)
{
nextNonMainThreadFontAccessWarningCheck =
Environment.TickCount64 + NonMainThreadFontAccessWarningCheckInterval;
var stack = new StackTrace();
if (Service<PluginManager>.GetNullable()?.FindCallingPlugin(stack) is { } plugin)
{
if (!NonMainThreadFontAccessWarning.TryGetValue(plugin, out _))
{
NonMainThreadFontAccessWarning.Add(plugin, new());
Log.Warning(
"[IM] {pluginName}: Accessing fonts outside the main thread is deprecated.\n{stack}",
plugin.Name,
stack);
}
}
else
{
// Dalamud internal should be made safe right now
throw new InvalidOperationException("Attempted to access fonts outside the main thread.");
}
}
this.interfaceManager.EnqueueDeferredDispose(locked);
return locked.ImFont;
}
/// <summary>
/// Attempts to lock the fully constructed instance of <see cref="ImFontPtr"/> corresponding to the this
/// <see cref="IFontHandle"/>, for use in any thread.<br />
/// Modification of the font will exhibit undefined behavior if some other thread also uses the font.
/// </summary>
/// <param name="errorMessage">The error message, if any.</param>
/// <returns>
/// An instance of <see cref="IFontHandle.ImFontLocked"/> that <b>must</b> be disposed after use on success;
/// <c>null</c> with <paramref name="errorMessage"/> populated on failure.
/// </returns>
/// <exception cref="ObjectDisposedException">Still may be thrown.</exception>
public IFontHandle.ImFontLocked? TryLock(out string? errorMessage)
{
IFontHandleSubstance? prevSubstance = default;
while (true)
{
var substance = this.Manager.Substance;
// Does the associated IFontAtlas have a built substance?
if (substance is null)
{
errorMessage = "The font atlas has not been built yet.";
return null;
}
// Did we loop (because it did not have the requested font),
// and are the fetched substance same between loops?
if (substance == prevSubstance)
{
errorMessage = "The font atlas did not built the requested handle yet.";
return null;
}
prevSubstance = substance;
// Try to lock the substance.
try
{
substance.DataRoot.AddRef();
}
catch (ObjectDisposedException)
{
// If it got invalidated, it's probably because a new substance is incoming. Try again.
continue;
}
var fontPtr = substance.GetFontPtr(this);
if (fontPtr.IsNull())
{
// The font for the requested handle is unavailable. Release the reference and try again.
substance.DataRoot.Release();
continue;
}
// Transfer the ownership of reference.
errorMessage = null;
return new(fontPtr, substance.DataRoot);
}
}
/// <inheritdoc/>
public IFontHandle.ImFontLocked Lock() =>
this.TryLock(out var errorMessage) ?? throw new InvalidOperationException(errorMessage);
/// <inheritdoc/>
public IDisposable Push()
{
ThreadSafety.AssertMainThread();
// Warn if the client is not properly managing the pushed font stack.
var cumulativePresentCalls = this.interfaceManager.CumulativePresentCalls;
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 font = default(ImFontPtr);
if (this.TryLock(out _) is { } locked)
{
font = locked.ImFont;
this.interfaceManager.EnqueueDeferredDispose(locked);
}
var rented = SimplePushedFont.Rent(this.pushedFonts, font);
this.pushedFonts.Add(rented);
return rented;
}
/// <inheritdoc/>
public void Pop()
{
ThreadSafety.AssertMainThread();
this.pushedFonts[^1].Dispose();
}
/// <inheritdoc/>
public Task<IFontHandle> WaitAsync()
{
if (this.Available)
return Task.FromResult<IFontHandle>(this);
var tcs = new TaskCompletionSource<IFontHandle>();
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);
}
}
/// <summary>
/// Invokes <see cref="IFontHandle.ImFontChanged"/>.
/// </summary>
protected void InvokeImFontChanged() => this.ImFontChanged.InvokeSafely(this);
/// <summary>
/// Overrideable implementation for <see cref="IDisposable.Dispose"/>.
/// </summary>
/// <param name="disposing">If <c>true</c>, then the function is being called from <see cref="IDisposable.Dispose"/>.</param>
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
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);
this.ImFontChanged = null;
}
}
}

View file

@ -4,7 +4,6 @@ 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;
@ -16,8 +15,6 @@ using ImGuiNET;
using Lumina.Data.Files;
using Serilog;
using Vector4 = System.Numerics.Vector4;
namespace Dalamud.Interface.ManagedFontAtlas.Internals;
@ -25,7 +22,7 @@ namespace Dalamud.Interface.ManagedFontAtlas.Internals;
/// <summary>
/// A font handle that uses the game's built-in fonts, optionally with some styling.
/// </summary>
internal class GamePrebakedFontHandle : IFontHandle.IInternal
internal class GamePrebakedFontHandle : FontHandle
{
/// <summary>
/// The smallest value of <see cref="SeIconChar"/>.
@ -37,17 +34,13 @@ 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.
/// </summary>
/// <param name="manager">An instance of <see cref="IFontHandleManager"/>.</param>
/// <param name="style">Font to use.</param>
public GamePrebakedFontHandle(IFontHandleManager manager, GameFontStyle style)
: base(manager)
{
if (!Enum.IsDefined(style.FamilyAndSize) || style.FamilyAndSize == GameFontFamilyAndSize.Undefined)
throw new ArgumentOutOfRangeException(nameof(style), style, null);
@ -55,15 +48,9 @@ internal class GamePrebakedFontHandle : IFontHandle.IInternal
if (style.SizePt <= 0)
throw new ArgumentException($"{nameof(style.SizePt)} must be a positive number.", nameof(style));
this.manager = manager;
this.FontStyle = style;
}
/// <inheritdoc/>
public event Action<IFontHandle>? ImFontChanged;
private event Action<IFontHandle>? Disposed;
/// <summary>
/// Provider for <see cref="IDalamudTextureWrap"/> for `common/font/fontNN.tex`.
/// </summary>
@ -107,119 +94,6 @@ internal class GamePrebakedFontHandle : IFontHandle.IInternal
/// </summary>
public GameFontStyle FontStyle { get; }
/// <inheritdoc/>
public Exception? LoadException => this.ManagerNotDisposed.Substance?.GetBuildException(this);
/// <inheritdoc/>
public bool Available => this.ImFont.IsNotNullAndLoaded();
/// <inheritdoc/>
public ImFontPtr ImFont => this.ManagerNotDisposed.Substance?.GetFontPtr(this) ?? default;
private IFontHandleManager ManagerNotDisposed =>
this.manager ?? throw new ObjectDisposedException(nameof(GamePrebakedFontHandle));
/// <inheritdoc/>
public void Dispose()
{
this.manager?.FreeFontHandle(this);
this.manager = null;
this.Disposed?.InvokeSafely(this);
this.ImFontChanged = null;
}
/// <inheritdoc/>
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();
}
}
}
/// <inheritdoc/>
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()
{
if (this.Available)
return Task.FromResult<IFontHandle>(this);
var tcs = new TaskCompletionSource<IFontHandle>();
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);
}
}
/// <inheritdoc/>
public override string ToString() => $"{nameof(GamePrebakedFontHandle)}({this.FontStyle})";
@ -305,7 +179,7 @@ internal class GamePrebakedFontHandle : IFontHandle.IInternal
return;
foreach (var handle in hs.RelevantHandles)
handle.ImFontChanged?.InvokeSafely(handle);
handle.InvokeImFontChanged();
}
/// <inheritdoc/>

View file

@ -28,17 +28,14 @@ internal sealed class SimplePushedFont : IDisposable
/// </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)
public static SimplePushedFont Rent(List<IDisposable> stack, ImFontPtr fontPtr)
{
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)
if (fontPtr.IsNotNullAndLoaded())
{
rented.font = fontPtr;
ImGui.PushFont(fontPtr);

View file

@ -498,7 +498,7 @@ public sealed class UiBuilder : IDisposable
[Obsolete($"Use {nameof(this.FontAtlas)}.{nameof(IFontAtlas.NewGameFontHandle)} instead.", false)]
[Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)]
public GameFontHandle GetGameFontHandle(GameFontStyle style) => new(
(IFontHandle.IInternal)this.FontAtlas.NewGameFontHandle(style),
(GamePrebakedFontHandle)this.FontAtlas.NewGameFontHandle(style),
Service<FontAtlasFactory>.Get());
/// <summary>