Add IFontHandle.Lock and WaitAsync

This commit is contained in:
Soreepeong 2024-01-21 03:10:41 +09:00
parent 620f3999d2
commit d70b430e0d
9 changed files with 543 additions and 86 deletions

View file

@ -1,4 +1,5 @@
using System.Numerics; using System.Numerics;
using System.Threading.Tasks;
using Dalamud.Interface.ManagedFontAtlas; using Dalamud.Interface.ManagedFontAtlas;
using Dalamud.Interface.ManagedFontAtlas.Internals; using Dalamud.Interface.ManagedFontAtlas.Internals;
@ -28,6 +29,13 @@ public sealed class GameFontHandle : IFontHandle
this.fontAtlasFactory = fontAtlasFactory; this.fontAtlasFactory = fontAtlasFactory;
} }
/// <inheritdoc />
public event Action<IFontHandle> ImFontChanged
{
add => this.fontHandle.ImFontChanged += value;
remove => this.fontHandle.ImFontChanged -= value;
}
/// <inheritdoc /> /// <inheritdoc />
public Exception? LoadException => this.fontHandle.LoadException; public Exception? LoadException => this.fontHandle.LoadException;
@ -55,15 +63,21 @@ public sealed class GameFontHandle : IFontHandle
/// <inheritdoc /> /// <inheritdoc />
public void Dispose() => this.fontHandle.Dispose(); public void Dispose() => this.fontHandle.Dispose();
/// <inheritdoc />
public IFontHandle.ImFontLocked Lock() => this.fontHandle.Lock();
/// <summary> /// <summary>
/// Pushes the font. /// Pushes the font.
/// </summary> /// </summary>
/// <returns>An <see cref="IDisposable"/> that can be used to pop the font on dispose.</returns> /// <returns>An <see cref="IDisposable"/> that can be used to pop the font on dispose.</returns>
public IDisposable Push() => this.fontHandle.Push(); public IDisposable Push() => this.fontHandle.Push();
/// <inheritdoc/> /// <inheritdoc/>
IFontHandle.FontPopper IFontHandle.Push() => this.fontHandle.Push(); IFontHandle.FontPopper IFontHandle.Push() => this.fontHandle.Push();
/// <inheritdoc />
public Task<IFontHandle> WaitAsync() => this.fontHandle.WaitAsync();
/// <summary> /// <summary>
/// Creates a new <see cref="GameFontLayoutPlan.Builder"/>.<br /> /// Creates a new <see cref="GameFontLayoutPlan.Builder"/>.<br />
/// <br /> /// <br />

View file

@ -1,7 +1,9 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Immutable; using System.Collections.Immutable;
using System.Linq; using System.Linq;
using System.Numerics;
using System.Text; using System.Text;
using System.Threading.Tasks;
using Dalamud.Interface.GameFonts; using Dalamud.Interface.GameFonts;
using Dalamud.Interface.ManagedFontAtlas; using Dalamud.Interface.ManagedFontAtlas;
@ -11,6 +13,8 @@ using Dalamud.Utility;
using ImGuiNET; using ImGuiNET;
using Serilog;
namespace Dalamud.Interface.Internal.Windows.Data.Widgets; namespace Dalamud.Interface.Internal.Windows.Data.Widgets;
/// <summary> /// <summary>
@ -103,6 +107,10 @@ internal class GamePrebakedFontsTestWidget : IDataWindowWidget, IDisposable
minCapacity: 1024); minCapacity: 1024);
} }
ImGui.SameLine();
if (ImGui.Button("Test Lock"))
Task.Run(this.TestLock);
fixed (byte* labelPtr = "Test Input"u8) fixed (byte* labelPtr = "Test Input"u8)
{ {
if (ImGuiNative.igInputTextMultiline( if (ImGuiNative.igInputTextMultiline(
@ -210,4 +218,49 @@ internal class GamePrebakedFontsTestWidget : IDataWindowWidget, IDisposable
this.privateAtlas?.Dispose(); this.privateAtlas?.Dispose();
this.privateAtlas = null; this.privateAtlas = null;
} }
private async void TestLock()
{
if (this.fontHandles is not { } fontHandlesCopy)
return;
Log.Information($"{nameof(GamePrebakedFontsTestWidget)}: {nameof(this.TestLock)} waiting for build");
await using var garbage = new DisposeSafety.ScopedFinalizer();
var fonts = new List<ImFontPtr>();
IFontHandle[] handles;
try
{
handles = fontHandlesCopy.Values.SelectMany(x => x).Select(x => x.Handle.Value).ToArray();
foreach (var handle in handles)
{
await handle.WaitAsync();
var locked = handle.Lock();
garbage.Add(locked);
fonts.Add(locked);
}
}
catch (ObjectDisposedException)
{
Log.Information($"{nameof(GamePrebakedFontsTestWidget)}: {nameof(this.TestLock)} cancelled");
return;
}
Log.Information($"{nameof(GamePrebakedFontsTestWidget)}: {nameof(this.TestLock)} waiting in lock");
await Task.Delay(5000);
foreach (var (font, handle) in fonts.Zip(handles))
TestSingle(font, handle);
return;
unsafe void TestSingle(ImFontPtr fontPtr, IFontHandle handle)
{
var dim = default(Vector2);
var test = "Test string"u8;
fixed (byte* pTest = test)
ImGuiNative.ImFont_CalcTextSizeA(&dim, fontPtr, fontPtr.FontSize, float.MaxValue, 0, pTest, null, null);
Log.Information($"{nameof(GamePrebakedFontsTestWidget)}: {handle} => {dim}");
}
}
} }

View file

@ -1,4 +1,6 @@
using Dalamud.Utility; using System.Threading.Tasks;
using Dalamud.Utility;
using ImGuiNET; using ImGuiNET;
@ -9,6 +11,11 @@ namespace Dalamud.Interface.ManagedFontAtlas;
/// </summary> /// </summary>
public interface IFontHandle : IDisposable public interface IFontHandle : IDisposable
{ {
/// <summary>
/// Called when the built instance of <see cref="ImFontPtr"/> has been changed.
/// </summary>
event Action<IFontHandle> ImFontChanged;
/// <summary> /// <summary>
/// Represents a reference counting handle for fonts. Dalamud internal use only. /// Represents a reference counting handle for fonts. Dalamud internal use only.
/// </summary> /// </summary>
@ -18,7 +25,8 @@ public interface IFontHandle : IDisposable
/// Gets the font.<br /> /// Gets the font.<br />
/// Use of this properly is safe only from the UI thread.<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 /> /// 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. /// 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> /// </summary>
ImFontPtr ImFont { get; } ImFontPtr ImFont { get; }
} }
@ -29,11 +37,27 @@ public interface IFontHandle : IDisposable
Exception? LoadException { get; } Exception? LoadException { get; }
/// <summary> /// <summary>
/// Gets a value indicating whether this font is ready for use.<br /> /// Gets a value indicating whether this font is ready for use.
/// Use <see cref="Push"/> directly if you want to keep the current ImGui font if the font is not ready.
/// </summary> /// </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>
bool Available { get; } bool Available { get; }
/// <summary>
/// Locks the fully constructed instance of <see cref="ImFontPtr"/> corresponding to the this
/// <see cref="IFontHandle"/>, for <b>read-only</b> use in any thread.
/// </summary>
/// <returns>An instance of <see cref="ImFontLocked"/> that <b>must</b> be disposed after use.</returns>
/// <remarks>
/// Calling <see cref="IFontHandle"/>.<see cref="IDisposable.Dispose"/> will not unlock the <see cref="ImFontPtr"/>
/// locked by this function.
/// </remarks>
/// <exception cref="InvalidOperationException">If <see cref="Available"/> is <c>false</c>.</exception>
ImFontLocked Lock();
/// <summary> /// <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 using <see cref="ImGui.PushFont"/>, if available.<br />
/// Use <see cref="ImGui.GetFont"/> to access the current font.<br /> /// Use <see cref="ImGui.GetFont"/> to access the current font.<br />
@ -47,6 +71,54 @@ public interface IFontHandle : IDisposable
/// </remarks> /// </remarks>
FontPopper Push(); FontPopper Push();
/// <summary>
/// Waits for <see cref="Available"/> to become <c>true</c>.
/// </summary>
/// <returns>A task containing this <see cref="IFontHandle"/>.</returns>
Task<IFontHandle> WaitAsync();
/// <summary>
/// The wrapper for <see cref="ImFontPtr"/>, guaranteeing that the associated data will be available as long as
/// this struct is not disposed.
/// </summary>
public struct ImFontLocked : IDisposable
{
/// <summary>
/// The associated <see cref="ImFontPtr"/>.
/// </summary>
public ImFontPtr ImFont;
private IRefCountable? owner;
/// <summary>
/// Initializes a new instance of the <see cref="ImFontLocked"/> struct,
/// and incrase the reference count of <paramref name="owner"/>.
/// </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;
}
public static implicit operator ImFontPtr(ImFontLocked l) => l.ImFont;
public static unsafe implicit operator ImFont*(ImFontLocked l) => l.ImFont.NativePtr;
/// <inheritdoc/>
public void Dispose()
{
if (this.owner is null)
return;
this.owner.Release();
this.owner = null;
this.ImFont = default;
}
}
/// <summary> /// <summary>
/// The wrapper for popping fonts. /// The wrapper for popping fonts.
/// </summary> /// </summary>

View file

@ -1,5 +1,6 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks;
using Dalamud.Interface.Utility; using Dalamud.Interface.Utility;
using Dalamud.Logging.Internal; using Dalamud.Logging.Internal;
@ -27,6 +28,11 @@ internal class DelegateFontHandle : IFontHandle.IInternal
this.CallOnBuildStepChange = callOnBuildStepChange; this.CallOnBuildStepChange = callOnBuildStepChange;
} }
/// <inheritdoc/>
public event Action<IFontHandle>? ImFontChanged;
private event Action<IFontHandle>? Disposed;
/// <summary> /// <summary>
/// Gets the function to be called on build step changes. /// Gets the function to be called on build step changes.
/// </summary> /// </summary>
@ -49,11 +55,76 @@ internal class DelegateFontHandle : IFontHandle.IInternal
{ {
this.manager?.FreeFontHandle(this); this.manager?.FreeFontHandle(this);
this.manager = null; 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/> /// <inheritdoc/>
public IFontHandle.FontPopper Push() => new(this.ImFont, this.Available); public IFontHandle.FontPopper Push() => new(this.ImFont, this.Available);
/// <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> /// <summary>
/// Manager for <see cref="DelegateFontHandle"/>s. /// Manager for <see cref="DelegateFontHandle"/>s.
/// </summary> /// </summary>
@ -81,11 +152,7 @@ internal class DelegateFontHandle : IFontHandle.IInternal
public void Dispose() public void Dispose()
{ {
lock (this.syncRoot) lock (this.syncRoot)
{
this.handles.Clear(); this.handles.Clear();
this.Substance?.Dispose();
this.Substance = null;
}
} }
/// <inheritdoc cref="IFontAtlas.NewDelegateFontHandle"/> /// <inheritdoc cref="IFontAtlas.NewDelegateFontHandle"/>
@ -109,10 +176,20 @@ internal class DelegateFontHandle : IFontHandle.IInternal
} }
/// <inheritdoc/> /// <inheritdoc/>
public IFontHandleSubstance NewSubstance() public void InvokeFontHandleImFontChanged()
{
if (this.Substance is not HandleSubstance hs)
return;
foreach (var handle in hs.RelevantHandles)
handle.ImFontChanged?.InvokeSafely(handle);
}
/// <inheritdoc/>
public IFontHandleSubstance NewSubstance(IRefCountable dataRoot)
{ {
lock (this.syncRoot) lock (this.syncRoot)
return new HandleSubstance(this, this.handles.ToArray()); return new HandleSubstance(this, dataRoot, this.handles.ToArray());
} }
} }
@ -123,9 +200,6 @@ internal class DelegateFontHandle : IFontHandle.IInternal
{ {
private static readonly ModuleLog Log = new($"{nameof(DelegateFontHandle)}.{nameof(HandleSubstance)}"); private static readonly ModuleLog Log = new($"{nameof(DelegateFontHandle)}.{nameof(HandleSubstance)}");
// Not owned by this class. Do not dispose.
private readonly DelegateFontHandle[] relevantHandles;
// Owned by this class, but ImFontPtr values still do not belong to this. // Owned by this class, but ImFontPtr values still do not belong to this.
private readonly Dictionary<DelegateFontHandle, ImFontPtr> fonts = new(); private readonly Dictionary<DelegateFontHandle, ImFontPtr> fonts = new();
private readonly Dictionary<DelegateFontHandle, Exception?> buildExceptions = new(); private readonly Dictionary<DelegateFontHandle, Exception?> buildExceptions = new();
@ -134,13 +208,29 @@ internal class DelegateFontHandle : IFontHandle.IInternal
/// Initializes a new instance of the <see cref="HandleSubstance"/> class. /// Initializes a new instance of the <see cref="HandleSubstance"/> class.
/// </summary> /// </summary>
/// <param name="manager">The manager.</param> /// <param name="manager">The manager.</param>
/// <param name="dataRoot">The data root.</param>
/// <param name="relevantHandles">The relevant handles.</param> /// <param name="relevantHandles">The relevant handles.</param>
public HandleSubstance(IFontHandleManager manager, DelegateFontHandle[] relevantHandles) public HandleSubstance(
IFontHandleManager manager,
IRefCountable dataRoot,
DelegateFontHandle[] relevantHandles)
{ {
// We do not call dataRoot.AddRef; this object is dependant on lifetime of dataRoot.
this.Manager = manager; this.Manager = manager;
this.relevantHandles = relevantHandles; this.DataRoot = dataRoot;
this.RelevantHandles = relevantHandles;
} }
/// <summary>
/// Gets the relevant handles.
/// </summary>
// Not owned by this class. Do not dispose.
public DelegateFontHandle[] RelevantHandles { get; }
/// <inheritdoc/>
public IRefCountable DataRoot { get; }
/// <inheritdoc/> /// <inheritdoc/>
public IFontHandleManager Manager { get; } public IFontHandleManager Manager { get; }
@ -171,7 +261,7 @@ internal class DelegateFontHandle : IFontHandle.IInternal
public void OnPreBuild(IFontAtlasBuildToolkitPreBuild toolkitPreBuild) public void OnPreBuild(IFontAtlasBuildToolkitPreBuild toolkitPreBuild)
{ {
var fontsVector = toolkitPreBuild.Fonts; var fontsVector = toolkitPreBuild.Fonts;
foreach (var k in this.relevantHandles) foreach (var k in this.RelevantHandles)
{ {
var fontCountPrevious = fontsVector.Length; var fontCountPrevious = fontsVector.Length;
@ -288,7 +378,7 @@ internal class DelegateFontHandle : IFontHandle.IInternal
/// <inheritdoc/> /// <inheritdoc/>
public void OnPostBuild(IFontAtlasBuildToolkitPostBuild toolkitPostBuild) public void OnPostBuild(IFontAtlasBuildToolkitPostBuild toolkitPostBuild)
{ {
foreach (var k in this.relevantHandles) foreach (var k in this.RelevantHandles)
{ {
if (!this.fonts[k].IsNotNullAndLoaded()) if (!this.fonts[k].IsNotNullAndLoaded())
continue; continue;
@ -315,7 +405,7 @@ internal class DelegateFontHandle : IFontHandle.IInternal
/// <inheritdoc/> /// <inheritdoc/>
public void OnPostPromotion(IFontAtlasBuildToolkitPostPromotion toolkitPostPromotion) public void OnPostPromotion(IFontAtlasBuildToolkitPostPromotion toolkitPostPromotion)
{ {
foreach (var k in this.relevantHandles) foreach (var k in this.RelevantHandles)
{ {
if (!this.fonts[k].IsNotNullAndLoaded()) if (!this.fonts[k].IsNotNullAndLoaded())
continue; continue;

View file

@ -43,68 +43,67 @@ internal sealed partial class FontAtlasFactory
private static readonly Task<FontAtlasBuiltData> EmptyTask = Task.FromResult(default(FontAtlasBuiltData)); private static readonly Task<FontAtlasBuiltData> EmptyTask = Task.FromResult(default(FontAtlasBuiltData));
private struct FontAtlasBuiltData : IDisposable private class FontAtlasBuiltData : IRefCountable
{ {
public readonly DalamudFontAtlas? Owner; private readonly List<IDalamudTextureWrap> wraps;
public readonly ImFontAtlasPtr Atlas; private readonly List<IFontHandleSubstance> substances;
public readonly float Scale;
public bool IsBuildInProgress; private int refCount;
private readonly List<IDalamudTextureWrap>? wraps; public unsafe FontAtlasBuiltData(DalamudFontAtlas owner, float scale)
private readonly List<IFontHandleSubstance>? substances;
private readonly DisposeSafety.ScopedFinalizer? garbage;
public unsafe FontAtlasBuiltData(
DalamudFontAtlas owner,
IEnumerable<IFontHandleSubstance> substances,
float scale)
{ {
this.Owner = owner; this.Owner = owner;
this.Scale = scale; this.Scale = scale;
this.garbage = new(); this.Garbage = new();
this.refCount = 1;
try try
{ {
var substancesList = this.substances = new(); var substancesList = this.substances = new();
foreach (var s in substances) this.Garbage.Add(() => substancesList.Clear());
substancesList.Add(this.garbage.Add(s));
this.garbage.Add(() => substancesList.Clear());
var wrapsCopy = this.wraps = new(); var wrapsCopy = this.wraps = new();
this.garbage.Add(() => wrapsCopy.Clear()); this.Garbage.Add(() => wrapsCopy.Clear());
var atlasPtr = ImGuiNative.ImFontAtlas_ImFontAtlas(); var atlasPtr = ImGuiNative.ImFontAtlas_ImFontAtlas();
this.Atlas = atlasPtr; this.Atlas = atlasPtr;
if (this.Atlas.NativePtr is null) if (this.Atlas.NativePtr is null)
throw new OutOfMemoryException($"Failed to allocate a new {nameof(ImFontAtlas)}."); throw new OutOfMemoryException($"Failed to allocate a new {nameof(ImFontAtlas)}.");
this.garbage.Add(() => ImGuiNative.ImFontAtlas_destroy(atlasPtr)); this.Garbage.Add(() => ImGuiNative.ImFontAtlas_destroy(atlasPtr));
this.IsBuildInProgress = true; this.IsBuildInProgress = true;
} }
catch catch
{ {
this.garbage.Dispose(); this.Garbage.Dispose();
throw; throw;
} }
} }
public readonly DisposeSafety.ScopedFinalizer Garbage => public DalamudFontAtlas? Owner { get; }
this.garbage ?? throw new ObjectDisposedException(nameof(FontAtlasBuiltData));
public readonly ImVectorWrapper<ImFontPtr> Fonts => this.Atlas.FontsWrapped(); public ImFontAtlasPtr Atlas { get; }
public readonly ImVectorWrapper<ImFontConfig> ConfigData => this.Atlas.ConfigDataWrapped(); public float Scale { get; }
public readonly ImVectorWrapper<ImFontAtlasTexture> ImTextures => this.Atlas.TexturesWrapped(); public bool IsBuildInProgress { get; set; }
public readonly IReadOnlyList<IDalamudTextureWrap> Wraps => public DisposeSafety.ScopedFinalizer Garbage { get; }
(IReadOnlyList<IDalamudTextureWrap>?)this.wraps ?? Array.Empty<IDalamudTextureWrap>();
public readonly IReadOnlyList<IFontHandleSubstance> Substances => public ImVectorWrapper<ImFontPtr> Fonts => this.Atlas.FontsWrapped();
(IReadOnlyList<IFontHandleSubstance>?)this.substances ?? Array.Empty<IFontHandleSubstance>();
public readonly void AddExistingTexture(IDalamudTextureWrap wrap) public ImVectorWrapper<ImFontConfig> ConfigData => this.Atlas.ConfigDataWrapped();
public ImVectorWrapper<ImFontAtlasTexture> ImTextures => this.Atlas.TexturesWrapped();
public IReadOnlyList<IDalamudTextureWrap> Wraps => this.wraps;
public IReadOnlyList<IFontHandleSubstance> Substances => this.substances;
public void InitialAddSubstance(IFontHandleSubstance substance) =>
this.substances.Add(this.Garbage.Add(substance));
public void AddExistingTexture(IDalamudTextureWrap wrap)
{ {
if (this.wraps is null) if (this.wraps is null)
throw new ObjectDisposedException(nameof(FontAtlasBuiltData)); throw new ObjectDisposedException(nameof(FontAtlasBuiltData));
@ -112,7 +111,7 @@ internal sealed partial class FontAtlasFactory
this.wraps.Add(this.Garbage.Add(wrap)); this.wraps.Add(this.Garbage.Add(wrap));
} }
public readonly int AddNewTexture(IDalamudTextureWrap wrap, bool disposeOnError) public int AddNewTexture(IDalamudTextureWrap wrap, bool disposeOnError)
{ {
if (this.wraps is null) if (this.wraps is null)
throw new ObjectDisposedException(nameof(FontAtlasBuiltData)); throw new ObjectDisposedException(nameof(FontAtlasBuiltData));
@ -160,27 +159,47 @@ internal sealed partial class FontAtlasFactory
return index; return index;
} }
public unsafe void Dispose() public int AddRef() => IRefCountable.AlterRefCount(1, ref this.refCount, out var newRefCount) switch
{ {
if (this.garbage is null) IRefCountable.RefCountResult.StillAlive => newRefCount,
return; IRefCountable.RefCountResult.AlreadyDisposed =>
throw new ObjectDisposedException(nameof(FontAtlasBuiltData)),
IRefCountable.RefCountResult.FinalRelease => throw new InvalidOperationException(),
_ => throw new InvalidOperationException(),
};
if (this.IsBuildInProgress) public unsafe int Release()
{
switch (IRefCountable.AlterRefCount(-1, ref this.refCount, out var newRefCount))
{ {
Log.Error( case IRefCountable.RefCountResult.StillAlive:
"[{name}] 0x{ptr:X}: Trying to dispose while build is in progress; waiting for build.\n" + return newRefCount;
"Stack:\n{trace}",
this.Owner?.Name ?? "<?>", case IRefCountable.RefCountResult.FinalRelease:
(nint)this.Atlas.NativePtr, if (this.IsBuildInProgress)
new StackTrace()); {
while (this.IsBuildInProgress) Log.Error(
Thread.Sleep(100); "[{name}] 0x{ptr:X}: Trying to dispose while build is in progress; waiting for build.\n" +
} "Stack:\n{trace}",
this.Owner?.Name ?? "<?>",
(nint)this.Atlas.NativePtr,
new StackTrace());
while (this.IsBuildInProgress)
Thread.Sleep(100);
}
#if VeryVerboseLog #if VeryVerboseLog
Log.Verbose("[{name}] 0x{ptr:X}: Disposing", this.Owner?.Name ?? "<?>", (nint)this.Atlas.NativePtr); Log.Verbose("[{name}] 0x{ptr:X}: Disposing", this.Owner?.Name ?? "<?>", (nint)this.Atlas.NativePtr);
#endif #endif
this.garbage.Dispose(); this.Garbage.Dispose();
return newRefCount;
case IRefCountable.RefCountResult.AlreadyDisposed:
throw new ObjectDisposedException(nameof(FontAtlasBuiltData));
default:
throw new InvalidOperationException();
}
} }
public BuildToolkit CreateToolkit(FontAtlasFactory factory, bool isAsync) public BuildToolkit CreateToolkit(FontAtlasFactory factory, bool isAsync)
@ -201,8 +220,8 @@ internal sealed partial class FontAtlasFactory
private readonly object syncRootPostPromotion = new(); private readonly object syncRootPostPromotion = new();
private readonly object syncRoot = new(); private readonly object syncRoot = new();
private Task<FontAtlasBuiltData> buildTask = EmptyTask; private Task<FontAtlasBuiltData?> buildTask = EmptyTask;
private FontAtlasBuiltData builtData; private FontAtlasBuiltData? builtData;
private int buildSuppressionCounter; private int buildSuppressionCounter;
private bool buildSuppressionSuppressed; private bool buildSuppressionSuppressed;
@ -275,7 +294,8 @@ internal sealed partial class FontAtlasFactory
lock (this.syncRoot) lock (this.syncRoot)
{ {
this.buildTask.ToDisposableIgnoreExceptions().Dispose(); this.buildTask.ToDisposableIgnoreExceptions().Dispose();
this.builtData.Dispose(); this.builtData?.Release();
this.builtData = null;
} }
} }
@ -303,7 +323,7 @@ internal sealed partial class FontAtlasFactory
get get
{ {
lock (this.syncRoot) lock (this.syncRoot)
return this.builtData.Atlas; return this.builtData?.Atlas ?? default;
} }
} }
@ -311,7 +331,7 @@ internal sealed partial class FontAtlasFactory
public Task BuildTask => this.buildTask; public Task BuildTask => this.buildTask;
/// <inheritdoc/> /// <inheritdoc/>
public bool HasBuiltAtlas => !this.builtData.Atlas.IsNull(); public bool HasBuiltAtlas => !(this.builtData?.Atlas.IsNull() ?? true);
/// <inheritdoc/> /// <inheritdoc/>
public bool IsGlobalScaled { get; } public bool IsGlobalScaled { get; }
@ -474,13 +494,13 @@ internal sealed partial class FontAtlasFactory
var rebuildIndex = ++this.buildIndex; var rebuildIndex = ++this.buildIndex;
return this.buildTask = this.buildTask.ContinueWith(BuildInner).Unwrap(); return this.buildTask = this.buildTask.ContinueWith(BuildInner).Unwrap();
async Task<FontAtlasBuiltData> BuildInner(Task<FontAtlasBuiltData> unused) async Task<FontAtlasBuiltData?> BuildInner(Task<FontAtlasBuiltData> unused)
{ {
Log.Verbose("[{name}] Building from {source}.", this.Name, nameof(this.BuildFontsAsync)); Log.Verbose("[{name}] Building from {source}.", this.Name, nameof(this.BuildFontsAsync));
lock (this.syncRoot) lock (this.syncRoot)
{ {
if (this.buildIndex != rebuildIndex) if (this.buildIndex != rebuildIndex)
return default; return null;
} }
var res = await this.RebuildFontsPrivate(true, scale); var res = await this.RebuildFontsPrivate(true, scale);
@ -512,8 +532,10 @@ internal sealed partial class FontAtlasFactory
return; return;
} }
this.builtData.ExplicitDisposeIgnoreExceptions(); var prevBuiltData = this.builtData;
this.builtData = data; this.builtData = data;
prevBuiltData.ExplicitDisposeIgnoreExceptions();
this.buildTask = EmptyTask; this.buildTask = EmptyTask;
foreach (var substance in data.Substances) foreach (var substance in data.Substances)
substance.Manager.Substance = substance; substance.Manager.Substance = substance;
@ -570,6 +592,9 @@ internal sealed partial class FontAtlasFactory
} }
} }
foreach (var substance in data.Substances)
substance.Manager.InvokeFontHandleImFontChanged();
#if VeryVerboseLog #if VeryVerboseLog
Log.Verbose("[{name}] Built from {source}.", this.Name, source); Log.Verbose("[{name}] Built from {source}.", this.Name, source);
#endif #endif
@ -610,12 +635,14 @@ internal sealed partial class FontAtlasFactory
var sw = new Stopwatch(); var sw = new Stopwatch();
sw.Start(); sw.Start();
var res = default(FontAtlasBuiltData); FontAtlasBuiltData? res = null;
nint atlasPtr = 0; nint atlasPtr = 0;
BuildToolkit? toolkit = null; BuildToolkit? toolkit = null;
try try
{ {
res = new(this, this.fontHandleManagers.Select(x => x.NewSubstance()), scale); res = new(this, scale);
foreach (var fhm in this.fontHandleManagers)
res.InitialAddSubstance(fhm.NewSubstance(res));
unsafe unsafe
{ {
atlasPtr = (nint)res.Atlas.NativePtr; atlasPtr = (nint)res.Atlas.NativePtr;
@ -646,9 +673,11 @@ internal sealed partial class FontAtlasFactory
res.IsBuildInProgress = false; res.IsBuildInProgress = false;
toolkit.Dispose(); toolkit.Dispose();
res.Dispose(); res.Release();
res = new(this, this.fontHandleManagers.Select(x => x.NewSubstance()), scale); res = new(this, scale);
foreach (var fhm in this.fontHandleManagers)
res.InitialAddSubstance(fhm.NewSubstance(res));
unsafe unsafe
{ {
atlasPtr = (nint)res.Atlas.NativePtr; atlasPtr = (nint)res.Atlas.NativePtr;
@ -715,8 +744,12 @@ internal sealed partial class FontAtlasFactory
nameof(this.RebuildFontsPrivateReal), nameof(this.RebuildFontsPrivateReal),
atlasPtr, atlasPtr,
sw.ElapsedMilliseconds); sw.ElapsedMilliseconds);
res.IsBuildInProgress = false; if (res is not null)
res.Dispose(); {
res.IsBuildInProgress = false;
res.Release();
}
throw; throw;
} }
finally finally

View file

@ -4,6 +4,7 @@ using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Linq; using System.Linq;
using System.Reactive.Disposables; using System.Reactive.Disposables;
using System.Threading.Tasks;
using Dalamud.Game.Text; using Dalamud.Game.Text;
using Dalamud.Interface.GameFonts; using Dalamud.Interface.GameFonts;
@ -53,6 +54,11 @@ internal class GamePrebakedFontHandle : IFontHandle.IInternal
this.FontStyle = style; this.FontStyle = style;
} }
/// <inheritdoc/>
public event Action<IFontHandle>? ImFontChanged;
private event Action<IFontHandle>? Disposed;
/// <summary> /// <summary>
/// Provider for <see cref="IDalamudTextureWrap"/> for `common/font/fontNN.tex`. /// Provider for <see cref="IDalamudTextureWrap"/> for `common/font/fontNN.tex`.
/// </summary> /// </summary>
@ -113,17 +119,86 @@ internal class GamePrebakedFontHandle : IFontHandle.IInternal
{ {
this.manager?.FreeFontHandle(this); this.manager?.FreeFontHandle(this);
this.manager = null; 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/> /// <inheritdoc/>
public IFontHandle.FontPopper Push() => new(this.ImFont, this.Available); public IFontHandle.FontPopper Push() => new(this.ImFont, this.Available);
/// <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})";
/// <summary> /// <summary>
/// Manager for <see cref="GamePrebakedFontHandle"/>s. /// Manager for <see cref="GamePrebakedFontHandle"/>s.
/// </summary> /// </summary>
internal sealed class HandleManager : IFontHandleManager internal sealed class HandleManager : IFontHandleManager
{ {
private readonly Dictionary<GameFontStyle, int> gameFontsRc = new(); private readonly Dictionary<GameFontStyle, int> gameFontsRc = new();
private readonly HashSet<GamePrebakedFontHandle> handles = new();
private readonly object syncRoot = new(); private readonly object syncRoot = new();
/// <summary> /// <summary>
@ -154,8 +229,7 @@ internal class GamePrebakedFontHandle : IFontHandle.IInternal
/// <inheritdoc/> /// <inheritdoc/>
public void Dispose() public void Dispose()
{ {
this.Substance?.Dispose(); // empty
this.Substance = null;
} }
/// <inheritdoc cref="IFontAtlas.NewGameFontHandle"/> /// <inheritdoc cref="IFontAtlas.NewGameFontHandle"/>
@ -165,6 +239,7 @@ internal class GamePrebakedFontHandle : IFontHandle.IInternal
bool suggestRebuild; bool suggestRebuild;
lock (this.syncRoot) lock (this.syncRoot)
{ {
this.handles.Add(handle);
this.gameFontsRc[style] = this.gameFontsRc.GetValueOrDefault(style, 0) + 1; this.gameFontsRc[style] = this.gameFontsRc.GetValueOrDefault(style, 0) + 1;
suggestRebuild = this.Substance?.GetFontPtr(handle).IsNotNullAndLoaded() is not true; suggestRebuild = this.Substance?.GetFontPtr(handle).IsNotNullAndLoaded() is not true;
} }
@ -183,6 +258,7 @@ internal class GamePrebakedFontHandle : IFontHandle.IInternal
lock (this.syncRoot) lock (this.syncRoot)
{ {
this.handles.Remove(ggfh);
if (!this.gameFontsRc.ContainsKey(ggfh.FontStyle)) if (!this.gameFontsRc.ContainsKey(ggfh.FontStyle))
return; return;
@ -192,10 +268,20 @@ internal class GamePrebakedFontHandle : IFontHandle.IInternal
} }
/// <inheritdoc/> /// <inheritdoc/>
public IFontHandleSubstance NewSubstance() public void InvokeFontHandleImFontChanged()
{
if (this.Substance is not HandleSubstance hs)
return;
foreach (var handle in hs.RelevantHandles)
handle.ImFontChanged?.InvokeSafely(handle);
}
/// <inheritdoc/>
public IFontHandleSubstance NewSubstance(IRefCountable dataRoot)
{ {
lock (this.syncRoot) lock (this.syncRoot)
return new HandleSubstance(this, this.gameFontsRc.Keys); return new HandleSubstance(this, dataRoot, this.handles.ToArray(), this.gameFontsRc.Keys);
} }
} }
@ -218,14 +304,32 @@ internal class GamePrebakedFontHandle : IFontHandle.IInternal
/// Initializes a new instance of the <see cref="HandleSubstance"/> class. /// Initializes a new instance of the <see cref="HandleSubstance"/> class.
/// </summary> /// </summary>
/// <param name="manager">The manager.</param> /// <param name="manager">The manager.</param>
/// <param name="dataRoot">The data root.</param>
/// <param name="relevantHandles">The relevant handles.</param>
/// <param name="gameFontStyles">The game font styles.</param> /// <param name="gameFontStyles">The game font styles.</param>
public HandleSubstance(HandleManager manager, IEnumerable<GameFontStyle> gameFontStyles) public HandleSubstance(
HandleManager manager,
IRefCountable dataRoot,
GamePrebakedFontHandle[] relevantHandles,
IEnumerable<GameFontStyle> gameFontStyles)
{ {
// We do not call dataRoot.AddRef; this object is dependant on lifetime of dataRoot.
this.handleManager = manager; this.handleManager = manager;
Service<InterfaceManager>.Get(); this.DataRoot = dataRoot;
this.RelevantHandles = relevantHandles;
this.gameFontStyles = new(gameFontStyles); this.gameFontStyles = new(gameFontStyles);
} }
/// <summary>
/// Gets the relevant handles.
/// </summary>
// Not owned by this class. Do not dispose.
public GamePrebakedFontHandle[] RelevantHandles { get; }
/// <inheritdoc/>
public IRefCountable DataRoot { get; }
/// <inheritdoc/> /// <inheritdoc/>
public IFontHandleManager Manager => this.handleManager; public IFontHandleManager Manager => this.handleManager;
@ -240,6 +344,7 @@ internal class GamePrebakedFontHandle : IFontHandle.IInternal
/// <inheritdoc/> /// <inheritdoc/>
public void Dispose() public void Dispose()
{ {
// empty
} }
/// <summary> /// <summary>

View file

@ -1,3 +1,5 @@
using Dalamud.Utility;
namespace Dalamud.Interface.ManagedFontAtlas.Internals; namespace Dalamud.Interface.ManagedFontAtlas.Internals;
/// <summary> /// <summary>
@ -27,6 +29,12 @@ internal interface IFontHandleManager : IDisposable
/// <summary> /// <summary>
/// Creates a new substance of the font atlas. /// Creates a new substance of the font atlas.
/// </summary> /// </summary>
/// <param name="dataRoot">The data root.</param>
/// <returns>The new substance.</returns> /// <returns>The new substance.</returns>
IFontHandleSubstance NewSubstance(); IFontHandleSubstance NewSubstance(IRefCountable dataRoot);
/// <summary>
/// Invokes <see cref="IFontHandle.ImFontChanged"/>.
/// </summary>
void InvokeFontHandleImFontChanged();
} }

View file

@ -9,6 +9,11 @@ namespace Dalamud.Interface.ManagedFontAtlas.Internals;
/// </summary> /// </summary>
internal interface IFontHandleSubstance : IDisposable internal interface IFontHandleSubstance : IDisposable
{ {
/// <summary>
/// Gets the data root relevant to this instance of <see cref="IFontHandleSubstance"/>.
/// </summary>
IRefCountable DataRoot { get; }
/// <summary> /// <summary>
/// Gets the manager relevant to this instance of <see cref="IFontHandleSubstance"/>. /// Gets the manager relevant to this instance of <see cref="IFontHandleSubstance"/>.
/// </summary> /// </summary>

View file

@ -0,0 +1,77 @@
using System.Diagnostics;
using System.Threading;
namespace Dalamud.Utility;
/// <summary>
/// Interface for reference counting.
/// </summary>
internal interface IRefCountable : IDisposable
{
/// <summary>
/// Result for <see cref="IRefCountable.AlterRefCount"/>.
/// </summary>
public enum RefCountResult
{
/// <summary>
/// The object still has remaining references. No futher action should be done.
/// </summary>
StillAlive = 1,
/// <summary>
/// The last reference to the object has been released. The object should be fully released.
/// </summary>
FinalRelease = 2,
/// <summary>
/// The object already has been disposed. <see cref="ObjectDisposedException"/> may be thrown.
/// </summary>
AlreadyDisposed = 3,
}
/// <summary>
/// Adds a reference to this reference counted object.
/// </summary>
/// <returns>The new number of references.</returns>
int AddRef();
/// <summary>
/// Releases a reference from this reference counted object.<br />
/// When all references are released, the object will be fully disposed.
/// </summary>
/// <returns>The new number of references.</returns>
int Release();
/// <summary>
/// Alias for <see cref="Release()"/>.
/// </summary>
void IDisposable.Dispose() => this.Release();
/// <summary>
/// Alters <paramref name="refCount"/> by <paramref name="delta"/>.
/// </summary>
/// <param name="delta">The delta to the reference count.</param>
/// <param name="refCount">The reference to the reference count.</param>
/// <param name="newRefCount">The new reference count.</param>
/// <returns>The followup action that should be done.</returns>
public static RefCountResult AlterRefCount(int delta, ref int refCount, out int newRefCount)
{
Debug.Assert(delta is 1 or -1, "delta must be 1 or -1");
while (true)
{
var refCountCopy = refCount;
if (refCountCopy <= 0)
{
newRefCount = refCountCopy;
return RefCountResult.AlreadyDisposed;
}
newRefCount = refCountCopy + delta;
if (refCountCopy != Interlocked.CompareExchange(ref refCount, newRefCount, refCountCopy))
continue;
return newRefCount == 0 ? RefCountResult.FinalRelease : RefCountResult.StillAlive;
}
}
}