Merge pull request #1619 from Soreepeong/feature/ifontatlas-lockable-fonts

Make `IFontHandle` lockable, and add font change event
This commit is contained in:
goat 2024-01-23 08:58:31 +01:00 committed by GitHub
commit e20daed848
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 893 additions and 146 deletions

View file

@ -69,6 +69,10 @@ namespace Dalamud.CorePlugin
this.Interface.UiBuilder.Draw += this.OnDraw;
this.Interface.UiBuilder.OpenConfigUi += this.OnOpenConfigUi;
this.Interface.UiBuilder.OpenMainUi += this.OnOpenMainUi;
this.Interface.UiBuilder.DefaultFontHandle.ImFontChanged += fc =>
{
Log.Information($"CorePlugin : DefaultFontHandle.ImFontChanged called {fc}");
};
Service<CommandManager>.Get().AddHandler("/coreplug", new(this.OnCommand) { HelpMessage = "Access the plugin." });

View file

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

View file

@ -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,14 +63,20 @@ 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 void Pop() => this.fontHandle.Pop();
/// <inheritdoc />
public Task<IFontHandle> WaitAsync() => this.fontHandle.WaitAsync();
/// <summary>
/// Creates a new <see cref="GameFontLayoutPlan.Builder"/>.<br />

View file

@ -13,7 +13,6 @@ using Dalamud.Game.ClientState.Keys;
using Dalamud.Game.Internal.DXGI;
using Dalamud.Hooking;
using Dalamud.Hooking.WndProcHook;
using Dalamud.Interface.GameFonts;
using Dalamud.Interface.Internal.ManagedAsserts;
using Dalamud.Interface.Internal.Notifications;
using Dalamud.Interface.ManagedFontAtlas;
@ -87,9 +86,6 @@ internal class InterfaceManager : IDisposable, IServiceType
private Hook<ResizeBuffersDelegate>? resizeBuffersHook;
private IFontAtlas? dalamudAtlas;
private IFontHandle.IInternal? defaultFontHandle;
private IFontHandle.IInternal? iconFontHandle;
private IFontHandle.IInternal? monoFontHandle;
// can't access imgui IO before first present call
private bool lastWantCapture = false;
@ -131,19 +127,34 @@ 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!.ImFont.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!.ImFont.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!.ImFont.OrElse(ImGui.GetIO().FontDefault);
/// <summary>
/// Gets the default font handle.
/// </summary>
public IFontHandle.IInternal? DefaultFontHandle { get; private set; }
/// <summary>
/// Gets the icon font handle.
/// </summary>
public IFontHandle.IInternal? IconFontHandle { get; private set; }
/// <summary>
/// Gets the mono font handle.
/// </summary>
public IFontHandle.IInternal? MonoFontHandle { get; private set; }
/// <summary>
/// Gets or sets the pointer to ImGui.IO(), when it was last used.
@ -219,6 +230,11 @@ internal class InterfaceManager : IDisposable, IServiceType
/// </summary>
public Task FontBuildTask => WhenFontsReady().dalamudAtlas!.BuildTask;
/// <summary>
/// Gets the number of calls to <see cref="PresentDetour"/> so far.
/// </summary>
public long CumulativePresentCalls { get; private set; }
/// <summary>
/// Dispose of managed and unmanaged resources.
/// </summary>
@ -636,6 +652,8 @@ internal class InterfaceManager : IDisposable, IServiceType
*/
private IntPtr PresentDetour(IntPtr swapChain, uint syncInterval, uint presentFlags)
{
this.CumulativePresentCalls++;
Debug.Assert(this.presentHook is not null, "How did PresentDetour get called when presentHook is null?");
Debug.Assert(this.dalamudAtlas is not null, "dalamudAtlas should have been set already");
@ -691,9 +709,9 @@ internal class InterfaceManager : IDisposable, IServiceType
.CreateFontAtlas(nameof(InterfaceManager), FontAtlasAutoRebuildMode.Disable);
using (this.dalamudAtlas.SuppressAutoRebuild())
{
this.defaultFontHandle = (IFontHandle.IInternal)this.dalamudAtlas.NewDelegateFontHandle(
this.DefaultFontHandle = (IFontHandle.IInternal)this.dalamudAtlas.NewDelegateFontHandle(
e => e.OnPreBuild(tk => tk.AddDalamudDefaultFont(DefaultFontSizePx)));
this.iconFontHandle = (IFontHandle.IInternal)this.dalamudAtlas.NewDelegateFontHandle(
this.IconFontHandle = (IFontHandle.IInternal)this.dalamudAtlas.NewDelegateFontHandle(
e => e.OnPreBuild(
tk => tk.AddFontAwesomeIconFont(
new()
@ -702,7 +720,7 @@ internal class InterfaceManager : IDisposable, IServiceType
GlyphMinAdvanceX = DefaultFontSizePx,
GlyphMaxAdvanceX = DefaultFontSizePx,
})));
this.monoFontHandle = (IFontHandle.IInternal)this.dalamudAtlas.NewDelegateFontHandle(
this.MonoFontHandle = (IFontHandle.IInternal)this.dalamudAtlas.NewDelegateFontHandle(
e => e.OnPreBuild(
tk => tk.AddDalamudAssetFont(
DalamudAsset.InconsolataRegular,
@ -714,13 +732,16 @@ internal class InterfaceManager : IDisposable, IServiceType
// Do not use DefaultFont, IconFont, and MonoFont.
// Use font handles directly.
using var defaultFont = this.DefaultFontHandle.Lock();
using var monoFont = this.MonoFontHandle.Lock();
// Fill missing glyphs in MonoFont from DefaultFont
tk.CopyGlyphsAcrossFonts(this.defaultFontHandle.ImFont, this.monoFontHandle.ImFont, true);
tk.CopyGlyphsAcrossFonts(defaultFont, monoFont, true);
// Update default font
unsafe
{
ImGui.GetIO().NativePtr->FontDefault = this.defaultFontHandle.ImFont;
ImGui.GetIO().NativePtr->FontDefault = defaultFont;
}
// Broadcast to auto-rebuilding instances

View file

@ -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(
@ -155,6 +163,7 @@ internal class GamePrebakedFontsTestWidget : IDataWindowWidget, IDisposable
.ToArray());
var offsetX = ImGui.CalcTextSize("99.9pt").X + (ImGui.GetStyle().FramePadding.X * 2);
var counter = 0;
foreach (var (family, items) in this.fontHandles)
{
if (!ImGui.CollapsingHeader($"{family} Family"))
@ -180,10 +189,21 @@ internal class GamePrebakedFontsTestWidget : IDataWindowWidget, IDisposable
{
if (!this.useGlobalScale)
ImGuiNative.igSetWindowFontScale(1 / ImGuiHelpers.GlobalScale);
using var pushPop = handle.Value.Push();
ImGuiNative.igTextUnformatted(
this.testStringBuffer.Data,
this.testStringBuffer.Data + this.testStringBuffer.Length);
if (counter++ % 2 == 0)
{
using var pushPop = handle.Value.Push();
ImGuiNative.igTextUnformatted(
this.testStringBuffer.Data,
this.testStringBuffer.Data + this.testStringBuffer.Length);
}
else
{
handle.Value.Push();
ImGuiNative.igTextUnformatted(
this.testStringBuffer.Data,
this.testStringBuffer.Data + this.testStringBuffer.Length);
handle.Value.Pop();
}
}
}
finally
@ -210,4 +230,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}");
}
}
}

View file

@ -69,6 +69,7 @@ internal class SettingsWindow : Window
var fontAtlasFactory = Service<FontAtlasFactory>.Get();
var rebuildFont = fontAtlasFactory.UseAxis != configuration.UseAxisFontsFromGame;
rebuildFont |= !Equals(ImGui.GetIO().FontGlobalScale, configuration.GlobalUiScale);
ImGui.GetIO().FontGlobalScale = configuration.GlobalUiScale;
fontAtlasFactory.UseAxisOverride = null;

View file

@ -122,6 +122,10 @@ public interface IFontAtlas : IDisposable
/// Note that <see cref="BuildTask"/> would not necessarily get changed from calling this function.
/// </summary>
/// <exception cref="InvalidOperationException">If <see cref="AutoRebuildMode"/> is <see cref="FontAtlasAutoRebuildMode.Async"/>.</exception>
/// <remarks>
/// Using this method will block the main thread on rebuilding fonts, effectively calling
/// <see cref="BuildFontsImmediately"/> from the main thread. Consider migrating to <see cref="BuildFontsAsync"/>.
/// </remarks>
void BuildFontsOnNextFrame();
/// <summary>

View file

@ -54,11 +54,11 @@ public interface IFontAtlasBuildToolkitPreBuild : IFontAtlasBuildToolkit
/// <summary>
/// Adds a font from memory region allocated using <see cref="ImGuiHelpers.AllocateMemory"/>.<br />
/// <strong>It WILL crash if you try to use a memory pointer allocated in some other way.</strong><br />
/// <strong>
/// <b>It WILL crash if you try to use a memory pointer allocated in some other way.</b><br />
/// <b>
/// Do NOT call <see cref="ImGuiNative.igMemFree"/> on the <paramref name="dataPointer"/> once this function has
/// been called, unless <paramref name="freeOnException"/> is set and the function has thrown an error.
/// </strong>
/// </b>
/// </summary>
/// <param name="dataPointer">Memory address for the data allocated using <see cref="ImGuiHelpers.AllocateMemory"/>.</param>
/// <param name="dataSize">The size of the font file..</param>
@ -81,9 +81,11 @@ public interface IFontAtlasBuildToolkitPreBuild : IFontAtlasBuildToolkit
/// <summary>
/// Adds a font from memory region allocated using <see cref="ImGuiHelpers.AllocateMemory"/>.<br />
/// <strong>It WILL crash if you try to use a memory pointer allocated in some other way.</strong><br />
/// <strong>Do NOT call <see cref="ImGuiNative.igMemFree"/> on the <paramref name="dataPointer"/> once this
/// function has been called.</strong>
/// <b>It WILL crash if you try to use a memory pointer allocated in some other way.</b><br />
/// <b>
/// Do NOT call <see cref="ImGuiNative.igMemFree"/> on the <paramref name="dataPointer"/> once this function has
/// been called, unless <paramref name="freeOnException"/> is set and the function has thrown an error.
/// </b>
/// </summary>
/// <param name="dataPointer">Memory address for the data allocated using <see cref="ImGuiHelpers.AllocateMemory"/>.</param>
/// <param name="dataSize">The size of the font file..</param>

View file

@ -1,4 +1,6 @@
using Dalamud.Utility;
using System.Threading.Tasks;
using Dalamud.Utility;
using ImGuiNET;
@ -9,6 +11,16 @@ namespace Dalamud.Interface.ManagedFontAtlas;
/// </summary>
public interface IFontHandle : IDisposable
{
/// <summary>
/// Called when the built instance of <see cref="ImFontPtr"/> has been changed.<br />
/// This event will be invoked on the same thread with
/// <see cref="IFontAtlas"/>.<see cref="IFontAtlas.BuildStepChange"/>,
/// when the build step is <see cref="FontAtlasBuildStep.PostPromotion"/>.<br />
/// See <see cref="IFontAtlas.BuildFontsOnNextFrame"/>, <see cref="IFontAtlas.BuildFontsImmediately"/>, and
/// <see cref="IFontAtlas.BuildFontsAsync"/>.
/// </summary>
event Action<IFontHandle> ImFontChanged;
/// <summary>
/// Represents a reference counting handle for fonts. Dalamud internal use only.
/// </summary>
@ -18,7 +30,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,54 +42,92 @@ 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>
/// Pushes the current font into ImGui font stack using <see cref="ImGui.PushFont"/>, if available.<br />
/// Locks 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>
/// <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, if available.<br />
/// Use <see cref="ImGui.GetFont"/> to access the current font.<br />
/// You may not access the font once you dispose this object.
/// </summary>
/// <returns>A disposable object that will call <see cref="ImGui.PopFont"/>(1) on dispose.</returns>
/// <returns>A disposable object that will pop the font on dispose.</returns>
/// <exception cref="InvalidOperationException">If called outside of the main thread.</exception>
/// <remarks>
/// Only intended for use with <c>using</c> keywords, such as <c>using (handle.Push())</c>.<br />
/// Should you store or transfer the return value to somewhere else, use <see cref="IDisposable"/> as the type.
/// This function uses <see cref="ImGui.PushFont"/>, and may do extra things.
/// Use <see cref="IDisposable.Dispose"/> or <see cref="Pop"/> to undo this operation.
/// Do not use <see cref="ImGui.PopFont"/>.
/// </remarks>
FontPopper Push();
IDisposable Push();
/// <summary>
/// The wrapper for popping fonts.
/// Pops the font pushed to ImGui using <see cref="Push"/>, cleaning up any extra information as needed.
/// </summary>
public struct FontPopper : IDisposable
void Pop();
/// <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
{
private int count;
/// <summary>
/// The associated <see cref="ImFontPtr"/>.
/// </summary>
public ImFontPtr ImFont;
private IRefCountable? owner;
/// <summary>
/// Initializes a new instance of the <see cref="FontPopper"/> struct.
/// Initializes a new instance of the <see cref="ImFontLocked"/> struct,
/// and incrase the reference count of <paramref name="owner"/>.
/// </summary>
/// <param name="fontPtr">The font to push.</param>
/// <param name="push">Whether to push.</param>
internal FontPopper(ImFontPtr fontPtr, bool push)
/// <param name="imFont">The contained font.</param>
/// <param name="owner">The owner.</param>
internal ImFontLocked(ImFontPtr imFont, IRefCountable owner)
{
if (!push)
return;
ThreadSafety.AssertMainThread();
this.count = 1;
ImGui.PushFont(fontPtr);
owner.AddRef();
this.ImFont = imFont;
this.owner = owner;
}
/// <inheritdoc />
public static implicit operator ImFontPtr(ImFontLocked l) => l.ImFont;
public static unsafe implicit operator ImFont*(ImFontLocked l) => l.ImFont.NativePtr;
/// <inheritdoc/>
public void Dispose()
{
ThreadSafety.AssertMainThread();
if (this.owner is null)
return;
while (this.count-- > 0)
ImGui.PopFont();
this.owner.Release();
this.owner = null;
this.ImFont = default;
}
}
}

View file

@ -1,12 +1,16 @@
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>
@ -14,7 +18,10 @@ namespace Dalamud.Interface.ManagedFontAtlas.Internals;
/// </summary>
internal class DelegateFontHandle : IFontHandle.IInternal
{
private readonly List<IDisposable> pushedFonts = new(8);
private IFontHandleManager? manager;
private long lastCumulativePresentCalls;
/// <summary>
/// Initializes a new instance of the <see cref="DelegateFontHandle"/> class.
@ -27,6 +34,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>
@ -47,12 +59,105 @@ internal class DelegateFontHandle : IFontHandle.IInternal
/// <inheritdoc/>
public void Dispose()
{
if (this.pushedFonts.Count > 0)
Log.Warning($"{nameof(IFontHandle)}.{nameof(IDisposable.Dispose)}: fonts were still in a stack.");
this.manager?.FreeFontHandle(this);
this.manager = null;
this.Disposed?.InvokeSafely(this);
this.ImFontChanged = null;
}
/// <inheritdoc/>
public IFontHandle.FontPopper Push() => new(this.ImFont, this.Available);
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.
@ -81,11 +186,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 +210,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 +234,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 +242,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 +295,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 +412,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 +439,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;

View file

@ -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

View file

@ -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;
@ -15,6 +16,8 @@ using ImGuiNET;
using Lumina.Data.Files;
using Serilog;
using Vector4 = System.Numerics.Vector4;
namespace Dalamud.Interface.ManagedFontAtlas.Internals;
@ -34,7 +37,10 @@ internal class GamePrebakedFontHandle : IFontHandle.IInternal
/// </summary>
public static readonly char SeIconCharMax = (char)Enum.GetValues<SeIconChar>().Max();
private readonly List<IDisposable> pushedFonts = new(8);
private IFontHandleManager? manager;
private long lastCumulativePresentCalls;
/// <summary>
/// Initializes a new instance of the <see cref="GamePrebakedFontHandle"/> class.
@ -53,6 +59,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,10 +124,104 @@ internal class GamePrebakedFontHandle : IFontHandle.IInternal
{
this.manager?.FreeFontHandle(this);
this.manager = null;
this.Disposed?.InvokeSafely(this);
this.ImFontChanged = null;
}
/// <inheritdoc/>
public IFontHandle.FontPopper Push() => new(this.ImFont, this.Available);
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})";
/// <summary>
/// Manager for <see cref="GamePrebakedFontHandle"/>s.
@ -124,6 +229,7 @@ internal class GamePrebakedFontHandle : IFontHandle.IInternal
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 +260,7 @@ internal class GamePrebakedFontHandle : IFontHandle.IInternal
/// <inheritdoc/>
public void Dispose()
{
this.Substance?.Dispose();
this.Substance = null;
// empty
}
/// <inheritdoc cref="IFontAtlas.NewGameFontHandle"/>
@ -165,6 +270,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 +289,7 @@ internal class GamePrebakedFontHandle : IFontHandle.IInternal
lock (this.syncRoot)
{
this.handles.Remove(ggfh);
if (!this.gameFontsRc.ContainsKey(ggfh.FontStyle))
return;
@ -192,10 +299,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 +335,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 +375,7 @@ internal class GamePrebakedFontHandle : IFontHandle.IInternal
/// <inheritdoc/>
public void Dispose()
{
// empty
}
/// <summary>

View file

@ -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();
}

View file

@ -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>

View file

@ -0,0 +1,78 @@
using System.Collections.Generic;
using System.Diagnostics;
using Dalamud.Interface.Utility;
using ImGuiNET;
using Microsoft.Extensions.ObjectPool;
using Serilog;
namespace Dalamud.Interface.ManagedFontAtlas.Internals;
/// <summary>
/// Reusable font push/popper.
/// </summary>
internal sealed class SimplePushedFont : IDisposable
{
// Using constructor instead of DefaultObjectPoolProvider, since we do not want the pool to call Dispose.
private static readonly ObjectPool<SimplePushedFont> Pool =
new DefaultObjectPool<SimplePushedFont>(new DefaultPooledObjectPolicy<SimplePushedFont>());
private List<IDisposable>? stack;
private ImFontPtr font;
/// <summary>
/// Pushes the font, and return an instance of <see cref="SimplePushedFont"/>.
/// </summary>
/// <param name="stack">The <see cref="IFontHandle"/>-private stack.</param>
/// <param name="fontPtr">The font pointer being pushed.</param>
/// <param name="push">Whether to push.</param>
/// <returns><c>this</c>.</returns>
public static SimplePushedFont Rent(List<IDisposable> stack, ImFontPtr fontPtr, bool push)
{
push &= !fontPtr.IsNull();
var rented = Pool.Get();
Debug.Assert(rented.font.IsNull(), "Rented object must not have its font set");
rented.stack = stack;
if (push)
{
rented.font = fontPtr;
ImGui.PushFont(fontPtr);
}
return rented;
}
/// <inheritdoc />
public unsafe void Dispose()
{
if (this.stack is null || !ReferenceEquals(this.stack[^1], this))
{
throw new InvalidOperationException("Tried to pop a non-pushed font.");
}
this.stack.RemoveAt(this.stack.Count - 1);
if (!this.font.IsNull())
{
if (ImGui.GetFont().NativePtr == this.font.NativePtr)
{
ImGui.PopFont();
}
else
{
Log.Warning(
$"{nameof(IFontHandle.Pop)}: The font currently being popped does not match the pushed font. " +
$"Doing nothing.");
}
}
this.font = default;
this.stack = null;
Pool.Return(this);
}
}

View file

@ -13,6 +13,7 @@ using Dalamud.Interface.Internal.ManagedAsserts;
using Dalamud.Interface.Internal.Notifications;
using Dalamud.Interface.ManagedFontAtlas;
using Dalamud.Interface.ManagedFontAtlas.Internals;
using Dalamud.Plugin.Internal.Types;
using Dalamud.Utility;
using ImGuiNET;
using ImGuiScene;
@ -41,6 +42,10 @@ public sealed class UiBuilder : IDisposable
private bool hasErrorWindow = false;
private bool lastFrameUiHideState = false;
private IFontHandle? defaultFontHandle;
private IFontHandle? iconFontHandle;
private IFontHandle? monoFontHandle;
/// <summary>
/// Initializes a new instance of the <see cref="UiBuilder"/> class and registers it.
/// You do not have to call this manually.
@ -99,21 +104,57 @@ public sealed class UiBuilder : IDisposable
/// <summary>
/// Gets or sets an action that is called any time ImGui fonts need to be rebuilt.<br/>
/// Any ImFontPtr objects that you store <strong>can be invalidated</strong> when fonts are rebuilt
/// Any ImFontPtr objects that you store <b>can be invalidated</b> when fonts are rebuilt
/// (at any time), so you should both reload your custom fonts and restore those
/// pointers inside this handler.
/// </summary>
[Obsolete($"Use {nameof(this.FontAtlas)} instead.", false)]
/// <remarks>
/// To add your custom font, use <see cref="FontAtlas"/>.<see cref="IFontAtlas.NewDelegateFontHandle"/> or
/// <see cref="IFontAtlas.NewGameFontHandle"/>.<br />
/// To be notified on font changes after fonts are built, use
/// <see cref="DefaultFontHandle"/>.<see cref="IFontHandle.ImFontChanged"/>.<br />
/// For all other purposes, use <see cref="FontAtlas"/>.<see cref="IFontAtlas.BuildStepChange"/>.<br />
/// <br />
/// Note that you will be calling above functions once, instead of every time inside a build step change callback.
/// For example, you can make all font handles from your plugin constructor, and then use the created handles during
/// <see cref="Draw"/> event, by using <see cref="IFontHandle.Push"/> in a scope.<br />
/// You may dispose your font handle anytime, as long as it's not in use in <see cref="Draw"/>.
/// Font handles may be constructed anytime, as long as the owner <see cref="IFontAtlas"/> or
/// <see cref="UiBuilder"/> is not disposed.<br />
/// <br />
/// If you were storing <see cref="ImFontPtr"/>, consider if the job can be achieved solely by using
/// <see cref="IFontHandle"/> without directly using an instance of <see cref="ImFontPtr"/>.<br />
/// If you do need it, evaluate if you need to access fonts outside the main thread.<br />
/// If it is the case, use <see cref="IFontHandle.Lock"/> to obtain a safe-to-access instance of
/// <see cref="ImFontPtr"/>, once <see cref="IFontHandle.WaitAsync"/> resolves.<br />
/// Otherwise, use <see cref="IFontHandle.Push"/>, and obtain the instance of <see cref="ImFontPtr"/> via
/// <see cref="ImGui.GetFont"/>. Do not let the <see cref="ImFontPtr"/> escape the <c>using</c> scope.<br />
/// <br />
/// If your plugin sets <see cref="PluginManifest.LoadRequiredState"/> to a non-default value, then
/// <see cref="DefaultFontHandle"/> should be accessed using
/// <see cref="RunWhenUiPrepared{T}(System.Func{T},bool)"/>, as the font handle member variables are only available
/// once drawing facilities are available.<br />
/// <br />
/// <b>Examples:</b><br />
/// * <see cref="InterfaceManager.ContinueConstruction"/>.<br />
/// * <see cref="Interface.Internal.Windows.Data.Widgets.GamePrebakedFontsTestWidget"/>.<br />
/// * <see cref="Interface.Internal.Windows.TitleScreenMenuWindow"/> ctor.<br />
/// * <see cref="Interface.Internal.Windows.Settings.Tabs.SettingsTabAbout"/>:
/// note how the construction of a new instance of <see cref="IFontAtlas"/> and
/// call of <see cref="IFontAtlas.NewGameFontHandle"/> are done in different functions,
/// without having to manually initiate font rebuild process.
/// </remarks>
[Obsolete("See remarks.", false)]
[Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)]
public event Action? BuildFonts;
/// <summary>
/// Gets or sets an action that is called any time right after ImGui fonts are rebuilt.<br/>
/// Any ImFontPtr objects that you store <strong>can be invalidated</strong> when fonts are rebuilt
/// Any ImFontPtr objects that you store <b>can be invalidated</b> when fonts are rebuilt
/// (at any time), so you should both reload your custom fonts and restore those
/// pointers inside this handler.
/// </summary>
[Obsolete($"Use {nameof(this.FontAtlas)} instead.", false)]
[Obsolete($"See remarks for {nameof(BuildFonts)}.", false)]
[Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)]
public event Action? AfterBuildFonts;
@ -143,6 +184,23 @@ public sealed class UiBuilder : IDisposable
/// Gets the default Dalamud font - supporting all game languages and icons.<br />
/// <strong>Accessing this static property outside of <see cref="Draw"/> is dangerous and not supported.</strong>
/// </summary>
public static ImFontPtr DefaultFont => InterfaceManager.DefaultFont;
/// <summary>
/// Gets the default Dalamud icon font based on FontAwesome 5 Free solid.<br />
/// <strong>Accessing this static property outside of <see cref="Draw"/> is dangerous and not supported.</strong>
/// </summary>
public static ImFontPtr IconFont => InterfaceManager.IconFont;
/// <summary>
/// Gets the default Dalamud monospaced font based on Inconsolata Regular.<br />
/// <strong>Accessing this static property outside of <see cref="Draw"/> is dangerous and not supported.</strong>
/// </summary>
public static ImFontPtr MonoFont => InterfaceManager.MonoFont;
/// <summary>
/// Gets the handle to the default Dalamud font - supporting all game languages and icons.
/// </summary>
/// <remarks>
/// A font handle corresponding to this font can be obtained with:
/// <code>
@ -151,11 +209,15 @@ public sealed class UiBuilder : IDisposable
/// tk => tk.AddDalamudDefaultFont(UiBuilder.DefaultFontSizePt)));
/// </code>
/// </remarks>
public static ImFontPtr DefaultFont => InterfaceManager.DefaultFont;
public IFontHandle DefaultFontHandle =>
this.defaultFontHandle ??=
this.scopedFinalizer.Add(
new FontHandleWrapper(
this.InterfaceManagerWithScene?.DefaultFontHandle
?? throw new InvalidOperationException("Scene is not yet ready.")));
/// <summary>
/// Gets the default Dalamud icon font based on FontAwesome 5 Free solid.<br />
/// <strong>Accessing this static property outside of <see cref="Draw"/> is dangerous and not supported.</strong>
/// Gets the default Dalamud icon font based on FontAwesome 5 Free solid.
/// </summary>
/// <remarks>
/// A font handle corresponding to this font can be obtained with:
@ -165,11 +227,15 @@ public sealed class UiBuilder : IDisposable
/// tk => tk.AddFontAwesomeIconFont(new() { SizePt = UiBuilder.DefaultFontSizePt })));
/// </code>
/// </remarks>
public static ImFontPtr IconFont => InterfaceManager.IconFont;
public IFontHandle IconFontHandle =>
this.iconFontHandle ??=
this.scopedFinalizer.Add(
new FontHandleWrapper(
this.InterfaceManagerWithScene?.IconFontHandle
?? throw new InvalidOperationException("Scene is not yet ready.")));
/// <summary>
/// Gets the default Dalamud monospaced font based on Inconsolata Regular.<br />
/// <strong>Accessing this static property outside of <see cref="Draw"/> is dangerous and not supported.</strong>
/// Gets the default Dalamud monospaced font based on Inconsolata Regular.
/// </summary>
/// <remarks>
/// A font handle corresponding to this font can be obtained with:
@ -181,7 +247,12 @@ public sealed class UiBuilder : IDisposable
/// new() { SizePt = UiBuilder.DefaultFontSizePt })));
/// </code>
/// </remarks>
public static ImFontPtr MonoFont => InterfaceManager.MonoFont;
public IFontHandle MonoFontHandle =>
this.monoFontHandle ??=
this.scopedFinalizer.Add(
new FontHandleWrapper(
this.InterfaceManagerWithScene?.MonoFontHandle
?? throw new InvalidOperationException("Scene is not yet ready.")));
/// <summary>
/// Gets the game's active Direct3D device.
@ -660,4 +731,48 @@ public sealed class UiBuilder : IDisposable
{
this.ResizeBuffers?.InvokeSafely();
}
private class FontHandleWrapper : IFontHandle
{
private IFontHandle? wrapped;
public FontHandleWrapper(IFontHandle wrapped)
{
this.wrapped = wrapped;
this.wrapped.ImFontChanged += this.WrappedOnImFontChanged;
}
public event Action<IFontHandle>? ImFontChanged;
public Exception? LoadException =>
this.wrapped!.LoadException ?? new ObjectDisposedException(nameof(FontHandleWrapper));
public bool Available => this.wrapped?.Available ?? false;
public void Dispose()
{
if (this.wrapped is not { } w)
return;
this.wrapped = null;
w.ImFontChanged -= this.WrappedOnImFontChanged;
// Note: do not dispose w; we do not own it
}
public IFontHandle.ImFontLocked Lock() =>
this.wrapped?.Lock() ?? throw new ObjectDisposedException(nameof(FontHandleWrapper));
public IDisposable Push() =>
this.wrapped?.Push() ?? throw new ObjectDisposedException(nameof(FontHandleWrapper));
public void Pop() => this.wrapped?.Pop();
public Task<IFontHandle> WaitAsync() =>
this.wrapped?.WaitAsync().ContinueWith(_ => (IFontHandle)this) ??
throw new ObjectDisposedException(nameof(FontHandleWrapper));
public override string ToString() => $"{nameof(FontHandleWrapper)}({this.wrapped})";
private void WrappedOnImFontChanged(IFontHandle obj) => this.ImFontChanged.InvokeSafely(this);
}
}

View file

@ -69,6 +69,14 @@ internal sealed class DalamudAssetManager : IServiceType, IDisposable, IDalamudA
.Select(x => x.ToContentDisposedTask()))
.ContinueWith(_ => loadTimings.Dispose()),
"Prevent Dalamud from loading more stuff, until we've ensured that all required assets are available.");
Task.WhenAll(
Enum.GetValues<DalamudAsset>()
.Where(x => x is not DalamudAsset.Empty4X4)
.Where(x => x.GetAttribute<DalamudAssetAttribute>()?.Required is false)
.Select(this.CreateStreamAsync)
.Select(x => x.ToContentDisposedTask()))
.ContinueWith(r => Log.Verbose($"Optional assets load state: {r}"));
}
/// <inheritdoc/>

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;
}
}
}