mirror of
https://github.com/goatcorp/Dalamud.git
synced 2025-12-12 18:27:23 +01:00
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:
parent
e20daed848
commit
5479149e79
8 changed files with 331 additions and 347 deletions
|
|
@ -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 />
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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/>
|
||||
|
|
|
|||
263
Dalamud/Interface/ManagedFontAtlas/Internals/FontHandle.cs
Normal file
263
Dalamud/Interface/ManagedFontAtlas/Internals/FontHandle.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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/>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue