mirror of
https://github.com/goatcorp/Dalamud.git
synced 2025-12-12 10:17:22 +01:00
Add IFontHandle.Lock and WaitAsync
This commit is contained in:
parent
620f3999d2
commit
d70b430e0d
9 changed files with 543 additions and 86 deletions
|
|
@ -1,4 +1,5 @@
|
|||
using System.Numerics;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Dalamud.Interface.ManagedFontAtlas;
|
||||
using Dalamud.Interface.ManagedFontAtlas.Internals;
|
||||
|
|
@ -28,6 +29,13 @@ public sealed class GameFontHandle : IFontHandle
|
|||
this.fontAtlasFactory = fontAtlasFactory;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public event Action<IFontHandle> ImFontChanged
|
||||
{
|
||||
add => this.fontHandle.ImFontChanged += value;
|
||||
remove => this.fontHandle.ImFontChanged -= value;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Exception? LoadException => this.fontHandle.LoadException;
|
||||
|
||||
|
|
@ -55,15 +63,21 @@ public sealed class GameFontHandle : IFontHandle
|
|||
/// <inheritdoc />
|
||||
public void Dispose() => this.fontHandle.Dispose();
|
||||
|
||||
/// <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>
|
||||
public IDisposable Push() => this.fontHandle.Push();
|
||||
|
||||
|
||||
/// <inheritdoc/>
|
||||
IFontHandle.FontPopper IFontHandle.Push() => this.fontHandle.Push();
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<IFontHandle> WaitAsync() => this.fontHandle.WaitAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="GameFontLayoutPlan.Builder"/>.<br />
|
||||
/// <br />
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Dalamud.Interface.GameFonts;
|
||||
using Dalamud.Interface.ManagedFontAtlas;
|
||||
|
|
@ -11,6 +13,8 @@ using Dalamud.Utility;
|
|||
|
||||
using ImGuiNET;
|
||||
|
||||
using Serilog;
|
||||
|
||||
namespace Dalamud.Interface.Internal.Windows.Data.Widgets;
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -103,6 +107,10 @@ internal class GamePrebakedFontsTestWidget : IDataWindowWidget, IDisposable
|
|||
minCapacity: 1024);
|
||||
}
|
||||
|
||||
ImGui.SameLine();
|
||||
if (ImGui.Button("Test Lock"))
|
||||
Task.Run(this.TestLock);
|
||||
|
||||
fixed (byte* labelPtr = "Test Input"u8)
|
||||
{
|
||||
if (ImGuiNative.igInputTextMultiline(
|
||||
|
|
@ -210,4 +218,49 @@ internal class GamePrebakedFontsTestWidget : IDataWindowWidget, IDisposable
|
|||
this.privateAtlas?.Dispose();
|
||||
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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
using Dalamud.Utility;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Dalamud.Utility;
|
||||
|
||||
using ImGuiNET;
|
||||
|
||||
|
|
@ -9,6 +11,11 @@ namespace Dalamud.Interface.ManagedFontAtlas;
|
|||
/// </summary>
|
||||
public interface IFontHandle : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Called when the built instance of <see cref="ImFontPtr"/> has been changed.
|
||||
/// </summary>
|
||||
event Action<IFontHandle> ImFontChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a reference counting handle for fonts. Dalamud internal use only.
|
||||
/// </summary>
|
||||
|
|
@ -18,7 +25,8 @@ public interface IFontHandle : IDisposable
|
|||
/// 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.
|
||||
/// 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; }
|
||||
}
|
||||
|
|
@ -29,11 +37,27 @@ public interface IFontHandle : IDisposable
|
|||
Exception? LoadException { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether this font is ready for use.<br />
|
||||
/// Use <see cref="Push"/> directly if you want to keep the current ImGui font if the font is not ready.
|
||||
/// 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>
|
||||
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>
|
||||
/// 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 />
|
||||
|
|
@ -47,6 +71,54 @@ public interface IFontHandle : IDisposable
|
|||
/// </remarks>
|
||||
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>
|
||||
/// The wrapper for popping fonts.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Dalamud.Interface.Utility;
|
||||
using Dalamud.Logging.Internal;
|
||||
|
|
@ -27,6 +28,11 @@ internal class DelegateFontHandle : IFontHandle.IInternal
|
|||
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>
|
||||
|
|
@ -49,11 +55,76 @@ internal class DelegateFontHandle : IFontHandle.IInternal
|
|||
{
|
||||
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 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>
|
||||
/// Manager for <see cref="DelegateFontHandle"/>s.
|
||||
/// </summary>
|
||||
|
|
@ -81,11 +152,7 @@ internal class DelegateFontHandle : IFontHandle.IInternal
|
|||
public void Dispose()
|
||||
{
|
||||
lock (this.syncRoot)
|
||||
{
|
||||
this.handles.Clear();
|
||||
this.Substance?.Dispose();
|
||||
this.Substance = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="IFontAtlas.NewDelegateFontHandle"/>
|
||||
|
|
@ -109,10 +176,20 @@ internal class DelegateFontHandle : IFontHandle.IInternal
|
|||
}
|
||||
|
||||
/// <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)
|
||||
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)}");
|
||||
|
||||
// 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.
|
||||
private readonly Dictionary<DelegateFontHandle, ImFontPtr> fonts = 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.
|
||||
/// </summary>
|
||||
/// <param name="manager">The manager.</param>
|
||||
/// <param name="dataRoot">The data root.</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.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/>
|
||||
public IFontHandleManager Manager { get; }
|
||||
|
||||
|
|
@ -171,7 +261,7 @@ internal class DelegateFontHandle : IFontHandle.IInternal
|
|||
public void OnPreBuild(IFontAtlasBuildToolkitPreBuild toolkitPreBuild)
|
||||
{
|
||||
var fontsVector = toolkitPreBuild.Fonts;
|
||||
foreach (var k in this.relevantHandles)
|
||||
foreach (var k in this.RelevantHandles)
|
||||
{
|
||||
var fontCountPrevious = fontsVector.Length;
|
||||
|
||||
|
|
@ -288,7 +378,7 @@ internal class DelegateFontHandle : IFontHandle.IInternal
|
|||
/// <inheritdoc/>
|
||||
public void OnPostBuild(IFontAtlasBuildToolkitPostBuild toolkitPostBuild)
|
||||
{
|
||||
foreach (var k in this.relevantHandles)
|
||||
foreach (var k in this.RelevantHandles)
|
||||
{
|
||||
if (!this.fonts[k].IsNotNullAndLoaded())
|
||||
continue;
|
||||
|
|
@ -315,7 +405,7 @@ internal class DelegateFontHandle : IFontHandle.IInternal
|
|||
/// <inheritdoc/>
|
||||
public void OnPostPromotion(IFontAtlasBuildToolkitPostPromotion toolkitPostPromotion)
|
||||
{
|
||||
foreach (var k in this.relevantHandles)
|
||||
foreach (var k in this.RelevantHandles)
|
||||
{
|
||||
if (!this.fonts[k].IsNotNullAndLoaded())
|
||||
continue;
|
||||
|
|
|
|||
|
|
@ -43,68 +43,67 @@ internal sealed partial class FontAtlasFactory
|
|||
|
||||
private static readonly Task<FontAtlasBuiltData> EmptyTask = Task.FromResult(default(FontAtlasBuiltData));
|
||||
|
||||
private struct FontAtlasBuiltData : IDisposable
|
||||
private class FontAtlasBuiltData : IRefCountable
|
||||
{
|
||||
public readonly DalamudFontAtlas? Owner;
|
||||
public readonly ImFontAtlasPtr Atlas;
|
||||
public readonly float Scale;
|
||||
private readonly List<IDalamudTextureWrap> wraps;
|
||||
private readonly List<IFontHandleSubstance> substances;
|
||||
|
||||
public bool IsBuildInProgress;
|
||||
private int refCount;
|
||||
|
||||
private readonly List<IDalamudTextureWrap>? wraps;
|
||||
private readonly List<IFontHandleSubstance>? substances;
|
||||
private readonly DisposeSafety.ScopedFinalizer? garbage;
|
||||
|
||||
public unsafe FontAtlasBuiltData(
|
||||
DalamudFontAtlas owner,
|
||||
IEnumerable<IFontHandleSubstance> substances,
|
||||
float scale)
|
||||
public unsafe FontAtlasBuiltData(DalamudFontAtlas owner, float scale)
|
||||
{
|
||||
this.Owner = owner;
|
||||
this.Scale = scale;
|
||||
this.garbage = new();
|
||||
this.Garbage = new();
|
||||
this.refCount = 1;
|
||||
|
||||
try
|
||||
{
|
||||
var substancesList = this.substances = new();
|
||||
foreach (var s in substances)
|
||||
substancesList.Add(this.garbage.Add(s));
|
||||
this.garbage.Add(() => substancesList.Clear());
|
||||
this.Garbage.Add(() => substancesList.Clear());
|
||||
|
||||
var wrapsCopy = this.wraps = new();
|
||||
this.garbage.Add(() => wrapsCopy.Clear());
|
||||
this.Garbage.Add(() => wrapsCopy.Clear());
|
||||
|
||||
var atlasPtr = ImGuiNative.ImFontAtlas_ImFontAtlas();
|
||||
this.Atlas = atlasPtr;
|
||||
if (this.Atlas.NativePtr is null)
|
||||
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;
|
||||
}
|
||||
catch
|
||||
{
|
||||
this.garbage.Dispose();
|
||||
this.Garbage.Dispose();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public readonly DisposeSafety.ScopedFinalizer Garbage =>
|
||||
this.garbage ?? throw new ObjectDisposedException(nameof(FontAtlasBuiltData));
|
||||
public DalamudFontAtlas? Owner { get; }
|
||||
|
||||
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 =>
|
||||
(IReadOnlyList<IDalamudTextureWrap>?)this.wraps ?? Array.Empty<IDalamudTextureWrap>();
|
||||
public DisposeSafety.ScopedFinalizer Garbage { get; }
|
||||
|
||||
public readonly IReadOnlyList<IFontHandleSubstance> Substances =>
|
||||
(IReadOnlyList<IFontHandleSubstance>?)this.substances ?? Array.Empty<IFontHandleSubstance>();
|
||||
public ImVectorWrapper<ImFontPtr> Fonts => this.Atlas.FontsWrapped();
|
||||
|
||||
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)
|
||||
throw new ObjectDisposedException(nameof(FontAtlasBuiltData));
|
||||
|
|
@ -112,7 +111,7 @@ internal sealed partial class FontAtlasFactory
|
|||
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)
|
||||
throw new ObjectDisposedException(nameof(FontAtlasBuiltData));
|
||||
|
|
@ -160,27 +159,47 @@ internal sealed partial class FontAtlasFactory
|
|||
return index;
|
||||
}
|
||||
|
||||
public unsafe void Dispose()
|
||||
public int AddRef() => IRefCountable.AlterRefCount(1, ref this.refCount, out var newRefCount) switch
|
||||
{
|
||||
if (this.garbage is null)
|
||||
return;
|
||||
IRefCountable.RefCountResult.StillAlive => newRefCount,
|
||||
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(
|
||||
"[{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);
|
||||
}
|
||||
case IRefCountable.RefCountResult.StillAlive:
|
||||
return newRefCount;
|
||||
|
||||
case IRefCountable.RefCountResult.FinalRelease:
|
||||
if (this.IsBuildInProgress)
|
||||
{
|
||||
Log.Error(
|
||||
"[{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
|
||||
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
|
||||
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)
|
||||
|
|
@ -201,8 +220,8 @@ internal sealed partial class FontAtlasFactory
|
|||
private readonly object syncRootPostPromotion = new();
|
||||
private readonly object syncRoot = new();
|
||||
|
||||
private Task<FontAtlasBuiltData> buildTask = EmptyTask;
|
||||
private FontAtlasBuiltData builtData;
|
||||
private Task<FontAtlasBuiltData?> buildTask = EmptyTask;
|
||||
private FontAtlasBuiltData? builtData;
|
||||
|
||||
private int buildSuppressionCounter;
|
||||
private bool buildSuppressionSuppressed;
|
||||
|
|
@ -275,7 +294,8 @@ internal sealed partial class FontAtlasFactory
|
|||
lock (this.syncRoot)
|
||||
{
|
||||
this.buildTask.ToDisposableIgnoreExceptions().Dispose();
|
||||
this.builtData.Dispose();
|
||||
this.builtData?.Release();
|
||||
this.builtData = null;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -303,7 +323,7 @@ internal sealed partial class FontAtlasFactory
|
|||
get
|
||||
{
|
||||
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;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool HasBuiltAtlas => !this.builtData.Atlas.IsNull();
|
||||
public bool HasBuiltAtlas => !(this.builtData?.Atlas.IsNull() ?? true);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsGlobalScaled { get; }
|
||||
|
|
@ -474,13 +494,13 @@ internal sealed partial class FontAtlasFactory
|
|||
var rebuildIndex = ++this.buildIndex;
|
||||
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));
|
||||
lock (this.syncRoot)
|
||||
{
|
||||
if (this.buildIndex != rebuildIndex)
|
||||
return default;
|
||||
return null;
|
||||
}
|
||||
|
||||
var res = await this.RebuildFontsPrivate(true, scale);
|
||||
|
|
@ -512,8 +532,10 @@ internal sealed partial class FontAtlasFactory
|
|||
return;
|
||||
}
|
||||
|
||||
this.builtData.ExplicitDisposeIgnoreExceptions();
|
||||
var prevBuiltData = this.builtData;
|
||||
this.builtData = data;
|
||||
prevBuiltData.ExplicitDisposeIgnoreExceptions();
|
||||
|
||||
this.buildTask = EmptyTask;
|
||||
foreach (var substance in data.Substances)
|
||||
substance.Manager.Substance = substance;
|
||||
|
|
@ -570,6 +592,9 @@ internal sealed partial class FontAtlasFactory
|
|||
}
|
||||
}
|
||||
|
||||
foreach (var substance in data.Substances)
|
||||
substance.Manager.InvokeFontHandleImFontChanged();
|
||||
|
||||
#if VeryVerboseLog
|
||||
Log.Verbose("[{name}] Built from {source}.", this.Name, source);
|
||||
#endif
|
||||
|
|
@ -610,12 +635,14 @@ internal sealed partial class FontAtlasFactory
|
|||
var sw = new Stopwatch();
|
||||
sw.Start();
|
||||
|
||||
var res = default(FontAtlasBuiltData);
|
||||
FontAtlasBuiltData? res = null;
|
||||
nint atlasPtr = 0;
|
||||
BuildToolkit? toolkit = null;
|
||||
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
|
||||
{
|
||||
atlasPtr = (nint)res.Atlas.NativePtr;
|
||||
|
|
@ -646,9 +673,11 @@ internal sealed partial class FontAtlasFactory
|
|||
|
||||
res.IsBuildInProgress = false;
|
||||
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
|
||||
{
|
||||
atlasPtr = (nint)res.Atlas.NativePtr;
|
||||
|
|
@ -715,8 +744,12 @@ internal sealed partial class FontAtlasFactory
|
|||
nameof(this.RebuildFontsPrivateReal),
|
||||
atlasPtr,
|
||||
sw.ElapsedMilliseconds);
|
||||
res.IsBuildInProgress = false;
|
||||
res.Dispose();
|
||||
if (res is not null)
|
||||
{
|
||||
res.IsBuildInProgress = false;
|
||||
res.Release();
|
||||
}
|
||||
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ 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;
|
||||
|
|
@ -53,6 +54,11 @@ internal class GamePrebakedFontHandle : IFontHandle.IInternal
|
|||
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>
|
||||
|
|
@ -113,17 +119,86 @@ internal class GamePrebakedFontHandle : IFontHandle.IInternal
|
|||
{
|
||||
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 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>
|
||||
/// Manager for <see cref="GamePrebakedFontHandle"/>s.
|
||||
/// </summary>
|
||||
internal sealed class HandleManager : IFontHandleManager
|
||||
{
|
||||
private readonly Dictionary<GameFontStyle, int> gameFontsRc = new();
|
||||
private readonly HashSet<GamePrebakedFontHandle> handles = new();
|
||||
private readonly object syncRoot = new();
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -154,8 +229,7 @@ internal class GamePrebakedFontHandle : IFontHandle.IInternal
|
|||
/// <inheritdoc/>
|
||||
public void Dispose()
|
||||
{
|
||||
this.Substance?.Dispose();
|
||||
this.Substance = null;
|
||||
// empty
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="IFontAtlas.NewGameFontHandle"/>
|
||||
|
|
@ -165,6 +239,7 @@ internal class GamePrebakedFontHandle : IFontHandle.IInternal
|
|||
bool suggestRebuild;
|
||||
lock (this.syncRoot)
|
||||
{
|
||||
this.handles.Add(handle);
|
||||
this.gameFontsRc[style] = this.gameFontsRc.GetValueOrDefault(style, 0) + 1;
|
||||
suggestRebuild = this.Substance?.GetFontPtr(handle).IsNotNullAndLoaded() is not true;
|
||||
}
|
||||
|
|
@ -183,6 +258,7 @@ internal class GamePrebakedFontHandle : IFontHandle.IInternal
|
|||
|
||||
lock (this.syncRoot)
|
||||
{
|
||||
this.handles.Remove(ggfh);
|
||||
if (!this.gameFontsRc.ContainsKey(ggfh.FontStyle))
|
||||
return;
|
||||
|
||||
|
|
@ -192,10 +268,20 @@ internal class GamePrebakedFontHandle : IFontHandle.IInternal
|
|||
}
|
||||
|
||||
/// <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)
|
||||
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.
|
||||
/// </summary>
|
||||
/// <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>
|
||||
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;
|
||||
Service<InterfaceManager>.Get();
|
||||
this.DataRoot = dataRoot;
|
||||
this.RelevantHandles = relevantHandles;
|
||||
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/>
|
||||
public IFontHandleManager Manager => this.handleManager;
|
||||
|
||||
|
|
@ -240,6 +344,7 @@ internal class GamePrebakedFontHandle : IFontHandle.IInternal
|
|||
/// <inheritdoc/>
|
||||
public void Dispose()
|
||||
{
|
||||
// empty
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
using Dalamud.Utility;
|
||||
|
||||
namespace Dalamud.Interface.ManagedFontAtlas.Internals;
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -27,6 +29,12 @@ internal interface IFontHandleManager : IDisposable
|
|||
/// <summary>
|
||||
/// Creates a new substance of the font atlas.
|
||||
/// </summary>
|
||||
/// <param name="dataRoot">The data root.</param>
|
||||
/// <returns>The new substance.</returns>
|
||||
IFontHandleSubstance NewSubstance();
|
||||
IFontHandleSubstance NewSubstance(IRefCountable dataRoot);
|
||||
|
||||
/// <summary>
|
||||
/// Invokes <see cref="IFontHandle.ImFontChanged"/>.
|
||||
/// </summary>
|
||||
void InvokeFontHandleImFontChanged();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,11 @@ namespace Dalamud.Interface.ManagedFontAtlas.Internals;
|
|||
/// </summary>
|
||||
internal interface IFontHandleSubstance : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the data root relevant to this instance of <see cref="IFontHandleSubstance"/>.
|
||||
/// </summary>
|
||||
IRefCountable DataRoot { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the manager relevant to this instance of <see cref="IFontHandleSubstance"/>.
|
||||
/// </summary>
|
||||
|
|
|
|||
77
Dalamud/Utility/IRefCountable.cs
Normal file
77
Dalamud/Utility/IRefCountable.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue