mirror of
https://github.com/goatcorp/Dalamud.git
synced 2025-12-12 10:17:22 +01:00
Merge pull request #1622 from Soreepeong/feature/ifontatlas-lock-safety
Lock font resources on Push and miscellaneous direct accesses
This commit is contained in:
commit
be965f8dd1
23 changed files with 631 additions and 692 deletions
|
|
@ -69,7 +69,7 @@ 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 =>
|
||||
this.Interface.UiBuilder.DefaultFontHandle.ImFontChanged += (fc, _) =>
|
||||
{
|
||||
Log.Information($"CorePlugin : DefaultFontHandle.ImFontChanged called {fc}");
|
||||
};
|
||||
|
|
|
|||
|
|
@ -15,22 +15,23 @@ namespace Dalamud.Interface.GameFonts;
|
|||
[Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)]
|
||||
public sealed class GameFontHandle : IFontHandle
|
||||
{
|
||||
private readonly IFontHandle.IInternal fontHandle;
|
||||
private readonly GamePrebakedFontHandle fontHandle;
|
||||
private readonly FontAtlasFactory fontAtlasFactory;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="GameFontHandle"/> class.
|
||||
/// Initializes a new instance of the <see cref="GameFontHandle"/> class.<br />
|
||||
/// Ownership of <paramref name="fontHandle"/> is transferred.
|
||||
/// </summary>
|
||||
/// <param name="fontHandle">The wrapped <see cref="IFontHandle"/>.</param>
|
||||
/// <param name="fontHandle">The wrapped <see cref="GamePrebakedFontHandle"/>.</param>
|
||||
/// <param name="fontAtlasFactory">An instance of <see cref="FontAtlasFactory"/>.</param>
|
||||
internal GameFontHandle(IFontHandle.IInternal fontHandle, FontAtlasFactory fontAtlasFactory)
|
||||
internal GameFontHandle(GamePrebakedFontHandle fontHandle, FontAtlasFactory fontAtlasFactory)
|
||||
{
|
||||
this.fontHandle = fontHandle;
|
||||
this.fontAtlasFactory = fontAtlasFactory;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public event Action<IFontHandle> ImFontChanged
|
||||
public event IFontHandle.ImFontChangedDelegate ImFontChanged
|
||||
{
|
||||
add => this.fontHandle.ImFontChanged += value;
|
||||
remove => this.fontHandle.ImFontChanged -= value;
|
||||
|
|
@ -42,9 +43,15 @@ public sealed class GameFontHandle : IFontHandle
|
|||
/// <inheritdoc />
|
||||
public bool Available => this.fontHandle.Available;
|
||||
|
||||
/// <inheritdoc cref="IFontHandle.IInternal.ImFont"/>
|
||||
[Obsolete($"Use {nameof(Push)}, and then use {nameof(ImGui.GetFont)} instead.", false)]
|
||||
public ImFontPtr ImFont => this.fontHandle.ImFont;
|
||||
/// <summary>
|
||||
/// Gets the font.<br />
|
||||
/// Use of this properly is safe only from the UI thread.<br />
|
||||
/// Use <see cref="IFontHandle.Push"/> if the intended purpose of this property is <see cref="ImGui.PushFont"/>.<br />
|
||||
/// Futures changes may make simple <see cref="ImGui.PushFont"/> not enough.<br />
|
||||
/// If you need to access a font outside the UI thread, use <see cref="IFontHandle.Lock"/>.
|
||||
/// </summary>
|
||||
[Obsolete($"Use {nameof(Push)}-{nameof(ImGui.GetFont)} or {nameof(Lock)} instead.", false)]
|
||||
public ImFontPtr ImFont => this.fontHandle.LockUntilPostFrame();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the font style. Only applicable for <see cref="GameFontHandle"/>.
|
||||
|
|
@ -64,12 +71,9 @@ public sealed class GameFontHandle : IFontHandle
|
|||
public void Dispose() => this.fontHandle.Dispose();
|
||||
|
||||
/// <inheritdoc />
|
||||
public IFontHandle.ImFontLocked Lock() => this.fontHandle.Lock();
|
||||
public ILockedImFont Lock() => this.fontHandle.Lock();
|
||||
|
||||
/// <summary>
|
||||
/// Pushes the font.
|
||||
/// </summary>
|
||||
/// <returns>An <see cref="IDisposable"/> that can be used to pop the font on dispose.</returns>
|
||||
/// <inheritdoc />
|
||||
public IDisposable Push() => this.fontHandle.Push();
|
||||
|
||||
/// <inheritdoc />
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
|
|
@ -20,8 +21,6 @@ using Dalamud.Interface.ManagedFontAtlas.Internals;
|
|||
using Dalamud.Interface.Style;
|
||||
using Dalamud.Interface.Utility;
|
||||
using Dalamud.Interface.Windowing;
|
||||
using Dalamud.Plugin.Internal;
|
||||
using Dalamud.Plugin.Internal.Types;
|
||||
using Dalamud.Utility;
|
||||
using Dalamud.Utility.Timing;
|
||||
using ImGuiNET;
|
||||
|
|
@ -63,15 +62,9 @@ internal class InterfaceManager : IDisposable, IServiceType
|
|||
/// </summary>
|
||||
public const float DefaultFontSizePx = (DefaultFontSizePt * 4.0f) / 3.0f;
|
||||
|
||||
private const int NonMainThreadFontAccessWarningCheckInterval = 10000;
|
||||
private static readonly ConditionalWeakTable<LocalPlugin, object> NonMainThreadFontAccessWarning = new();
|
||||
private static long nextNonMainThreadFontAccessWarningCheck;
|
||||
private readonly ConcurrentBag<DalamudTextureWrap> deferredDisposeTextures = new();
|
||||
private readonly ConcurrentBag<ILockedImFont> deferredDisposeImFontLockeds = new();
|
||||
|
||||
private readonly List<DalamudTextureWrap> deferredDisposeTextures = new();
|
||||
|
||||
[ServiceManager.ServiceDependency]
|
||||
private readonly Framework framework = Service<Framework>.Get();
|
||||
|
||||
[ServiceManager.ServiceDependency]
|
||||
private readonly WndProcHookManager wndProcHookManager = Service<WndProcHookManager>.Get();
|
||||
|
||||
|
|
@ -86,6 +79,7 @@ internal class InterfaceManager : IDisposable, IServiceType
|
|||
private Hook<ResizeBuffersDelegate>? resizeBuffersHook;
|
||||
|
||||
private IFontAtlas? dalamudAtlas;
|
||||
private ILockedImFont? defaultFontResourceLock;
|
||||
|
||||
// can't access imgui IO before first present call
|
||||
private bool lastWantCapture = false;
|
||||
|
|
@ -127,34 +121,37 @@ internal class InterfaceManager : IDisposable, IServiceType
|
|||
/// Gets the default ImGui font.<br />
|
||||
/// <strong>Accessing this static property outside of the main thread is dangerous and not supported.</strong>
|
||||
/// </summary>
|
||||
public static ImFontPtr DefaultFont => WhenFontsReady().DefaultFontHandle!.ImFont.OrElse(ImGui.GetIO().FontDefault);
|
||||
public static ImFontPtr DefaultFont =>
|
||||
WhenFontsReady().DefaultFontHandle!.LockUntilPostFrame().OrElse(ImGui.GetIO().FontDefault);
|
||||
|
||||
/// <summary>
|
||||
/// Gets an included FontAwesome icon font.<br />
|
||||
/// <strong>Accessing this static property outside of the main thread is dangerous and not supported.</strong>
|
||||
/// </summary>
|
||||
public static ImFontPtr IconFont => WhenFontsReady().IconFontHandle!.ImFont.OrElse(ImGui.GetIO().FontDefault);
|
||||
public static ImFontPtr IconFont =>
|
||||
WhenFontsReady().IconFontHandle!.LockUntilPostFrame().OrElse(ImGui.GetIO().FontDefault);
|
||||
|
||||
/// <summary>
|
||||
/// Gets an included monospaced font.<br />
|
||||
/// <strong>Accessing this static property outside of the main thread is dangerous and not supported.</strong>
|
||||
/// </summary>
|
||||
public static ImFontPtr MonoFont => WhenFontsReady().MonoFontHandle!.ImFont.OrElse(ImGui.GetIO().FontDefault);
|
||||
public static ImFontPtr MonoFont =>
|
||||
WhenFontsReady().MonoFontHandle!.LockUntilPostFrame().OrElse(ImGui.GetIO().FontDefault);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the default font handle.
|
||||
/// </summary>
|
||||
public IFontHandle.IInternal? DefaultFontHandle { get; private set; }
|
||||
public FontHandle? DefaultFontHandle { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the icon font handle.
|
||||
/// </summary>
|
||||
public IFontHandle.IInternal? IconFontHandle { get; private set; }
|
||||
public FontHandle? IconFontHandle { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the mono font handle.
|
||||
/// </summary>
|
||||
public IFontHandle.IInternal? MonoFontHandle { get; private set; }
|
||||
public FontHandle? MonoFontHandle { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the pointer to ImGui.IO(), when it was last used.
|
||||
|
|
@ -246,6 +243,8 @@ internal class InterfaceManager : IDisposable, IServiceType
|
|||
Disposer();
|
||||
|
||||
this.wndProcHookManager.PreWndProc -= this.WndProcHookManagerOnPreWndProc;
|
||||
this.defaultFontResourceLock?.Dispose(); // lock outlives handle and atlas
|
||||
this.defaultFontResourceLock = null;
|
||||
this.dalamudAtlas?.Dispose();
|
||||
this.scene?.Dispose();
|
||||
return;
|
||||
|
|
@ -408,6 +407,15 @@ internal class InterfaceManager : IDisposable, IServiceType
|
|||
this.deferredDisposeTextures.Add(wrap);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enqueue an <see cref="ILockedImFont"/> to be disposed at the end of the frame.
|
||||
/// </summary>
|
||||
/// <param name="locked">The disposable.</param>
|
||||
public void EnqueueDeferredDispose(in ILockedImFont locked)
|
||||
{
|
||||
this.deferredDisposeImFontLockeds.Add(locked);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get video memory information.
|
||||
/// </summary>
|
||||
|
|
@ -466,29 +474,6 @@ internal class InterfaceManager : IDisposable, IServiceType
|
|||
if (im?.dalamudAtlas is not { } atlas)
|
||||
throw new InvalidOperationException($"Tried to access fonts before {nameof(ContinueConstruction)} call.");
|
||||
|
||||
if (!ThreadSafety.IsMainThread && nextNonMainThreadFontAccessWarningCheck < Environment.TickCount64)
|
||||
{
|
||||
nextNonMainThreadFontAccessWarningCheck =
|
||||
Environment.TickCount64 + NonMainThreadFontAccessWarningCheckInterval;
|
||||
var stack = new StackTrace();
|
||||
if (Service<PluginManager>.GetNullable()?.FindCallingPlugin(stack) is { } plugin)
|
||||
{
|
||||
if (!NonMainThreadFontAccessWarning.TryGetValue(plugin, out _))
|
||||
{
|
||||
NonMainThreadFontAccessWarning.Add(plugin, new());
|
||||
Log.Warning(
|
||||
"[IM] {pluginName}: Accessing fonts outside the main thread is deprecated.\n{stack}",
|
||||
plugin.Name,
|
||||
stack);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Dalamud internal should be made safe right now
|
||||
throw new InvalidOperationException("Attempted to access fonts outside the main thread.");
|
||||
}
|
||||
}
|
||||
|
||||
if (!atlas.HasBuiltAtlas)
|
||||
atlas.BuildTask.GetAwaiter().GetResult();
|
||||
return im;
|
||||
|
|
@ -673,28 +658,38 @@ internal class InterfaceManager : IDisposable, IServiceType
|
|||
var pRes = this.presentHook!.Original(swapChain, syncInterval, presentFlags);
|
||||
|
||||
RenderImGui(this.scene!);
|
||||
this.DisposeTextures();
|
||||
this.CleanupPostImGuiRender();
|
||||
|
||||
return pRes;
|
||||
}
|
||||
|
||||
RenderImGui(this.scene!);
|
||||
this.DisposeTextures();
|
||||
this.CleanupPostImGuiRender();
|
||||
|
||||
return this.presentHook!.Original(swapChain, syncInterval, presentFlags);
|
||||
}
|
||||
|
||||
private void DisposeTextures()
|
||||
private void CleanupPostImGuiRender()
|
||||
{
|
||||
if (this.deferredDisposeTextures.Count > 0)
|
||||
if (!this.deferredDisposeTextures.IsEmpty)
|
||||
{
|
||||
Log.Verbose("[IM] Disposing {Count} textures", this.deferredDisposeTextures.Count);
|
||||
foreach (var texture in this.deferredDisposeTextures)
|
||||
var count = 0;
|
||||
while (this.deferredDisposeTextures.TryTake(out var d))
|
||||
{
|
||||
texture.RealDispose();
|
||||
count++;
|
||||
d.RealDispose();
|
||||
}
|
||||
|
||||
this.deferredDisposeTextures.Clear();
|
||||
Log.Verbose("[IM] Disposing {Count} textures", count);
|
||||
}
|
||||
|
||||
if (!this.deferredDisposeImFontLockeds.IsEmpty)
|
||||
{
|
||||
// Not logging; the main purpose of this is to keep resources used for rendering the frame to be kept
|
||||
// referenced until the resources are actually done being used, and it is expected that this will be
|
||||
// frequent.
|
||||
while (this.deferredDisposeImFontLockeds.TryTake(out var d))
|
||||
d.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -709,9 +704,9 @@ internal class InterfaceManager : IDisposable, IServiceType
|
|||
.CreateFontAtlas(nameof(InterfaceManager), FontAtlasAutoRebuildMode.Disable);
|
||||
using (this.dalamudAtlas.SuppressAutoRebuild())
|
||||
{
|
||||
this.DefaultFontHandle = (IFontHandle.IInternal)this.dalamudAtlas.NewDelegateFontHandle(
|
||||
this.DefaultFontHandle = (FontHandle)this.dalamudAtlas.NewDelegateFontHandle(
|
||||
e => e.OnPreBuild(tk => tk.AddDalamudDefaultFont(DefaultFontSizePx)));
|
||||
this.IconFontHandle = (IFontHandle.IInternal)this.dalamudAtlas.NewDelegateFontHandle(
|
||||
this.IconFontHandle = (FontHandle)this.dalamudAtlas.NewDelegateFontHandle(
|
||||
e => e.OnPreBuild(
|
||||
tk => tk.AddFontAwesomeIconFont(
|
||||
new()
|
||||
|
|
@ -720,37 +715,44 @@ internal class InterfaceManager : IDisposable, IServiceType
|
|||
GlyphMinAdvanceX = DefaultFontSizePx,
|
||||
GlyphMaxAdvanceX = DefaultFontSizePx,
|
||||
})));
|
||||
this.MonoFontHandle = (IFontHandle.IInternal)this.dalamudAtlas.NewDelegateFontHandle(
|
||||
this.MonoFontHandle = (FontHandle)this.dalamudAtlas.NewDelegateFontHandle(
|
||||
e => e.OnPreBuild(
|
||||
tk => tk.AddDalamudAssetFont(
|
||||
DalamudAsset.InconsolataRegular,
|
||||
new() { SizePx = DefaultFontSizePx })));
|
||||
this.dalamudAtlas.BuildStepChange += e => e.OnPostPromotion(
|
||||
this.dalamudAtlas.BuildStepChange += e => e.OnPostBuild(
|
||||
tk =>
|
||||
{
|
||||
// Note: the first call of this function is done outside the main thread; this is expected.
|
||||
// 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(defaultFont, monoFont, true);
|
||||
|
||||
// Update default font
|
||||
unsafe
|
||||
{
|
||||
ImGui.GetIO().NativePtr->FontDefault = defaultFont;
|
||||
}
|
||||
|
||||
// Broadcast to auto-rebuilding instances
|
||||
this.AfterBuildFonts?.Invoke();
|
||||
// Fill missing glyphs in MonoFont from DefaultFont.
|
||||
tk.CopyGlyphsAcrossFonts(
|
||||
tk.GetFont(this.DefaultFontHandle),
|
||||
tk.GetFont(this.MonoFontHandle),
|
||||
missingOnly: true);
|
||||
});
|
||||
this.DefaultFontHandle.ImFontChanged += (_, font) =>
|
||||
{
|
||||
var fontLocked = font.NewRef();
|
||||
Service<Framework>.Get().RunOnFrameworkThread(
|
||||
() =>
|
||||
{
|
||||
// Update the ImGui default font.
|
||||
unsafe
|
||||
{
|
||||
ImGui.GetIO().NativePtr->FontDefault = fontLocked.ImFont;
|
||||
}
|
||||
|
||||
// Update the reference to the resources of the default font.
|
||||
this.defaultFontResourceLock?.Dispose();
|
||||
this.defaultFontResourceLock = fontLocked;
|
||||
|
||||
// Broadcast to auto-rebuilding instances.
|
||||
this.AfterBuildFonts?.Invoke();
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
// This will wait for scene on its own. We just wait for this.dalamudAtlas.BuildTask in this.InitScene.
|
||||
_ = this.dalamudAtlas.BuildFontsAsync(false);
|
||||
_ = this.dalamudAtlas.BuildFontsAsync();
|
||||
|
||||
this.address.Setup(sigScanner);
|
||||
|
||||
|
|
|
|||
|
|
@ -249,7 +249,7 @@ internal class GamePrebakedFontsTestWidget : IDataWindowWidget, IDisposable
|
|||
await handle.WaitAsync();
|
||||
var locked = handle.Lock();
|
||||
garbage.Add(locked);
|
||||
fonts.Add(locked);
|
||||
fonts.Add(locked.ImFont);
|
||||
}
|
||||
}
|
||||
catch (ObjectDisposedException)
|
||||
|
|
|
|||
|
|
@ -7,32 +7,24 @@ namespace Dalamud.Interface.ManagedFontAtlas;
|
|||
/// </summary>
|
||||
public enum FontAtlasBuildStep
|
||||
{
|
||||
/// <summary>
|
||||
/// An invalid value. This should never be passed through event callbacks.
|
||||
/// </summary>
|
||||
Invalid,
|
||||
// Note: leave 0 alone; make default(FontAtlasBuildStep) not have a valid value
|
||||
|
||||
/// <summary>
|
||||
/// Called before calling <see cref="ImFontAtlasPtr.Build"/>.<br />
|
||||
/// Expect <see cref="IFontAtlasBuildToolkitPreBuild"/> to be passed.
|
||||
/// Expect <see cref="IFontAtlasBuildToolkitPreBuild"/> to be passed.<br />
|
||||
/// When called from <see cref="IFontAtlas.BuildStepChange"/>, this will be called <b>before</b> the delegates
|
||||
/// passed to <see cref="IFontAtlas.NewDelegateFontHandle"/>.
|
||||
/// </summary>
|
||||
PreBuild,
|
||||
PreBuild = 1,
|
||||
|
||||
/// <summary>
|
||||
/// Called after calling <see cref="ImFontAtlasPtr.Build"/>.<br />
|
||||
/// Expect <see cref="IFontAtlasBuildToolkitPostBuild"/> to be passed.<br />
|
||||
/// When called from <see cref="IFontAtlas.BuildStepChange"/>, this will be called <b>after</b> the delegates
|
||||
/// passed to <see cref="IFontAtlas.NewDelegateFontHandle"/>; you can do cross-font operations here.<br />
|
||||
/// <br />
|
||||
/// This callback is not guaranteed to happen after <see cref="PreBuild"/>,
|
||||
/// but it will never happen on its own.
|
||||
/// </summary>
|
||||
PostBuild,
|
||||
|
||||
/// <summary>
|
||||
/// Called after promoting staging font atlas to the actual atlas for <see cref="IFontAtlas"/>.<br />
|
||||
/// Expect <see cref="PostBuild"/> to be passed.<br />
|
||||
/// <br />
|
||||
/// This callback is not guaranteed to happen after <see cref="IFontAtlasBuildToolkitPostPromotion"/>,
|
||||
/// but it will never happen on its own.
|
||||
/// </summary>
|
||||
PostPromotion,
|
||||
PostBuild = 2,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,10 +6,9 @@ namespace Dalamud.Interface.ManagedFontAtlas;
|
|||
/// <param name="toolkit">A toolkit that may help you for font building steps.</param>
|
||||
/// <remarks>
|
||||
/// An implementation of <see cref="IFontAtlasBuildToolkit"/> may implement all of
|
||||
/// <see cref="IFontAtlasBuildToolkitPreBuild"/>, <see cref="IFontAtlasBuildToolkitPostBuild"/>, and
|
||||
/// <see cref="IFontAtlasBuildToolkitPostPromotion"/>.<br />
|
||||
/// <see cref="IFontAtlasBuildToolkitPreBuild"/> and <see cref="IFontAtlasBuildToolkitPostBuild"/>.<br />
|
||||
/// Either use <see cref="IFontAtlasBuildToolkit.BuildStep"/> to identify the build step, or use
|
||||
/// <see cref="FontAtlasBuildToolkitUtilities.OnPreBuild"/>, <see cref="FontAtlasBuildToolkitUtilities.OnPostBuild"/>,
|
||||
/// and <see cref="FontAtlasBuildToolkitUtilities.OnPostPromotion"/> for routing.
|
||||
/// <see cref="FontAtlasBuildToolkitUtilities.OnPreBuild"/> and <see cref="FontAtlasBuildToolkitUtilities.OnPostBuild"/>
|
||||
/// for routing.
|
||||
/// </remarks>
|
||||
public delegate void FontAtlasBuildStepDelegate(IFontAtlasBuildToolkit toolkit);
|
||||
|
|
|
|||
|
|
@ -113,21 +113,4 @@ public static class FontAtlasBuildToolkitUtilities
|
|||
action.Invoke((IFontAtlasBuildToolkitPostBuild)toolkit);
|
||||
return toolkit;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invokes <paramref name="action"/>
|
||||
/// if <see cref="IFontAtlasBuildToolkit.BuildStep"/> of <paramref name="toolkit"/>
|
||||
/// is <see cref="FontAtlasBuildStep.PostPromotion"/>.
|
||||
/// </summary>
|
||||
/// <param name="toolkit">The toolkit.</param>
|
||||
/// <param name="action">The action.</param>
|
||||
/// <returns>toolkit, for method chaining.</returns>
|
||||
public static IFontAtlasBuildToolkit OnPostPromotion(
|
||||
this IFontAtlasBuildToolkit toolkit,
|
||||
Action<IFontAtlasBuildToolkitPostPromotion> action)
|
||||
{
|
||||
if (toolkit.BuildStep is FontAtlasBuildStep.PostPromotion)
|
||||
action.Invoke((IFontAtlasBuildToolkitPostPromotion)toolkit);
|
||||
return toolkit;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -129,8 +129,7 @@ public interface IFontAtlas : IDisposable
|
|||
void BuildFontsOnNextFrame();
|
||||
|
||||
/// <summary>
|
||||
/// Rebuilds fonts immediately, on the current thread.<br />
|
||||
/// Even the callback for <see cref="FontAtlasBuildStep.PostPromotion"/> will be called on the same thread.
|
||||
/// Rebuilds fonts immediately, on the current thread.
|
||||
/// </summary>
|
||||
/// <exception cref="InvalidOperationException">If <see cref="AutoRebuildMode"/> is <see cref="FontAtlasAutoRebuildMode.Async"/>.</exception>
|
||||
void BuildFontsImmediately();
|
||||
|
|
@ -138,8 +137,7 @@ public interface IFontAtlas : IDisposable
|
|||
/// <summary>
|
||||
/// Rebuilds fonts asynchronously, on any thread.
|
||||
/// </summary>
|
||||
/// <param name="callPostPromotionOnMainThread">Call <see cref="FontAtlasBuildStep.PostPromotion"/> on the main thread.</param>
|
||||
/// <returns>The task.</returns>
|
||||
/// <exception cref="InvalidOperationException">If <see cref="AutoRebuildMode"/> is <see cref="FontAtlasAutoRebuildMode.OnNewFrame"/>.</exception>
|
||||
Task BuildFontsAsync(bool callPostPromotionOnMainThread = true);
|
||||
Task BuildFontsAsync();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -80,4 +80,12 @@ public interface IFontAtlasBuildToolkit
|
|||
/// </summary>
|
||||
/// <param name="action">The action to run on dispose.</param>
|
||||
void DisposeWithAtlas(Action action);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the instance of <see cref="ImFontPtr"/> corresponding to <paramref name="fontHandle"/>
|
||||
/// from <see cref="NewImAtlas"/>.
|
||||
/// </summary>
|
||||
/// <param name="fontHandle">The font handle.</param>
|
||||
/// <returns>The corresonding <see cref="ImFontPtr"/>, or default if not found.</returns>
|
||||
ImFontPtr GetFont(IFontHandle fontHandle);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,4 +23,28 @@ public interface IFontAtlasBuildToolkitPostBuild : IFontAtlasBuildToolkit
|
|||
/// <param name="disposeOnError">Dispose the wrap on error.</param>
|
||||
/// <returns>The texture index.</returns>
|
||||
int StoreTexture(IDalamudTextureWrap textureWrap, bool disposeOnError);
|
||||
|
||||
/// <summary>
|
||||
/// Copies glyphs across fonts, in a safer way.<br />
|
||||
/// If the font does not belong to the current atlas, this function is a no-op.
|
||||
/// </summary>
|
||||
/// <param name="source">Source font.</param>
|
||||
/// <param name="target">Target font.</param>
|
||||
/// <param name="missingOnly">Whether to copy missing glyphs only.</param>
|
||||
/// <param name="rebuildLookupTable">Whether to call target.BuildLookupTable().</param>
|
||||
/// <param name="rangeLow">Low codepoint range to copy.</param>
|
||||
/// <param name="rangeHigh">High codepoing range to copy.</param>
|
||||
void CopyGlyphsAcrossFonts(
|
||||
ImFontPtr source,
|
||||
ImFontPtr target,
|
||||
bool missingOnly,
|
||||
bool rebuildLookupTable = true,
|
||||
char rangeLow = ' ',
|
||||
char rangeHigh = '\uFFFE');
|
||||
|
||||
/// <summary>
|
||||
/// Calls <see cref="ImFontPtr.BuildLookupTable"/>, with some fixups.
|
||||
/// </summary>
|
||||
/// <param name="font">The font.</param>
|
||||
void BuildLookupTable(ImFontPtr font);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,33 +0,0 @@
|
|||
using ImGuiNET;
|
||||
|
||||
namespace Dalamud.Interface.ManagedFontAtlas;
|
||||
|
||||
/// <summary>
|
||||
/// Toolkit for use when the build state is <see cref="FontAtlasBuildStep.PostPromotion"/>.
|
||||
/// </summary>
|
||||
public interface IFontAtlasBuildToolkitPostPromotion : IFontAtlasBuildToolkit
|
||||
{
|
||||
/// <summary>
|
||||
/// Copies glyphs across fonts, in a safer way.<br />
|
||||
/// If the font does not belong to the current atlas, this function is a no-op.
|
||||
/// </summary>
|
||||
/// <param name="source">Source font.</param>
|
||||
/// <param name="target">Target font.</param>
|
||||
/// <param name="missingOnly">Whether to copy missing glyphs only.</param>
|
||||
/// <param name="rebuildLookupTable">Whether to call target.BuildLookupTable().</param>
|
||||
/// <param name="rangeLow">Low codepoint range to copy.</param>
|
||||
/// <param name="rangeHigh">High codepoing range to copy.</param>
|
||||
void CopyGlyphsAcrossFonts(
|
||||
ImFontPtr source,
|
||||
ImFontPtr target,
|
||||
bool missingOnly,
|
||||
bool rebuildLookupTable = true,
|
||||
char rangeLow = ' ',
|
||||
char rangeHigh = '\uFFFE');
|
||||
|
||||
/// <summary>
|
||||
/// Calls <see cref="ImFontPtr.BuildLookupTable"/>, with some fixups.
|
||||
/// </summary>
|
||||
/// <param name="font">The font.</param>
|
||||
void BuildLookupTable(ImFontPtr font);
|
||||
}
|
||||
|
|
@ -1,7 +1,5 @@
|
|||
using System.Threading.Tasks;
|
||||
|
||||
using Dalamud.Utility;
|
||||
|
||||
using ImGuiNET;
|
||||
|
||||
namespace Dalamud.Interface.ManagedFontAtlas;
|
||||
|
|
@ -12,29 +10,17 @@ namespace Dalamud.Interface.ManagedFontAtlas;
|
|||
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"/>.
|
||||
/// Delegate for <see cref="IFontHandle.ImFontChanged"/>.
|
||||
/// </summary>
|
||||
event Action<IFontHandle> ImFontChanged;
|
||||
/// <param name="fontHandle">The relevant font handle.</param>
|
||||
/// <param name="lockedFont">The locked font for this font handle, locked during the call of this delegate.</param>
|
||||
public delegate void ImFontChangedDelegate(IFontHandle fontHandle, ILockedImFont lockedFont);
|
||||
|
||||
/// <summary>
|
||||
/// Represents a reference counting handle for fonts. Dalamud internal use only.
|
||||
/// Called when the built instance of <see cref="ImFontPtr"/> has been changed.<br />
|
||||
/// This event can be invoked outside the main thread.
|
||||
/// </summary>
|
||||
internal interface IInternal : IFontHandle
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the font.<br />
|
||||
/// Use of this properly is safe only from the UI thread.<br />
|
||||
/// Use <see cref="IFontHandle.Push"/> if the intended purpose of this property is <see cref="ImGui.PushFont"/>.<br />
|
||||
/// Futures changes may make simple <see cref="ImGui.PushFont"/> not enough.<br />
|
||||
/// If you need to access a font outside the UI thread, consider using <see cref="IFontHandle.Lock"/>.
|
||||
/// </summary>
|
||||
ImFontPtr ImFont { get; }
|
||||
}
|
||||
event ImFontChangedDelegate ImFontChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the load exception, if it failed to load. Otherwise, it is null.
|
||||
|
|
@ -45,7 +31,6 @@ public interface IFontHandle : IDisposable
|
|||
/// Gets a value indicating whether this font is ready for use.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Once set to <c>true</c>, it will remain <c>true</c>.<br />
|
||||
/// Use <see cref="Push"/> directly if you want to keep the current ImGui font if the font is not ready.<br />
|
||||
/// Alternatively, use <see cref="WaitAsync"/> to wait for this property to become <c>true</c>.
|
||||
/// </remarks>
|
||||
|
|
@ -56,13 +41,13 @@ public interface IFontHandle : IDisposable
|
|||
/// <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>
|
||||
/// <returns>An instance of <see cref="ILockedImFont"/> 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();
|
||||
ILockedImFont Lock();
|
||||
|
||||
/// <summary>
|
||||
/// Pushes the current font into ImGui font stack, if available.<br />
|
||||
|
|
@ -88,46 +73,4 @@ public interface IFontHandle : IDisposable
|
|||
/// </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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
21
Dalamud/Interface/ManagedFontAtlas/ILockedImFont.cs
Normal file
21
Dalamud/Interface/ManagedFontAtlas/ILockedImFont.cs
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
using ImGuiNET;
|
||||
|
||||
namespace Dalamud.Interface.ManagedFontAtlas;
|
||||
|
||||
/// <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 interface ILockedImFont : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the associated <see cref="ImFontPtr"/>.
|
||||
/// </summary>
|
||||
ImFontPtr ImFont { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new instance of <see cref="ILockedImFont"/> with an additional reference to the owner.
|
||||
/// </summary>
|
||||
/// <returns>The new locked instance.</returns>
|
||||
ILockedImFont NewRef();
|
||||
}
|
||||
|
|
@ -1,164 +1,35 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Dalamud.Interface.Internal;
|
||||
using Dalamud.Interface.Utility;
|
||||
using Dalamud.Logging.Internal;
|
||||
using Dalamud.Utility;
|
||||
|
||||
using ImGuiNET;
|
||||
|
||||
using Serilog;
|
||||
|
||||
namespace Dalamud.Interface.ManagedFontAtlas.Internals;
|
||||
|
||||
/// <summary>
|
||||
/// A font handle representing a user-callback generated font.
|
||||
/// </summary>
|
||||
internal class DelegateFontHandle : IFontHandle.IInternal
|
||||
internal sealed class DelegateFontHandle : FontHandle
|
||||
{
|
||||
private readonly List<IDisposable> pushedFonts = new(8);
|
||||
|
||||
private IFontHandleManager? manager;
|
||||
private long lastCumulativePresentCalls;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DelegateFontHandle"/> class.
|
||||
/// </summary>
|
||||
/// <param name="manager">An instance of <see cref="IFontHandleManager"/>.</param>
|
||||
/// <param name="callOnBuildStepChange">Callback for <see cref="IFontAtlas.BuildStepChange"/>.</param>
|
||||
public DelegateFontHandle(IFontHandleManager manager, FontAtlasBuildStepDelegate callOnBuildStepChange)
|
||||
: base(manager)
|
||||
{
|
||||
this.manager = manager;
|
||||
this.CallOnBuildStepChange = callOnBuildStepChange;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public event Action<IFontHandle>? ImFontChanged;
|
||||
|
||||
private event Action<IFontHandle>? Disposed;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the function to be called on build step changes.
|
||||
/// </summary>
|
||||
public FontAtlasBuildStepDelegate CallOnBuildStepChange { get; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Exception? LoadException => this.ManagerNotDisposed.Substance?.GetBuildException(this);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool Available => this.ImFont.IsNotNullAndLoaded();
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ImFontPtr ImFont => this.ManagerNotDisposed.Substance?.GetFontPtr(this) ?? default;
|
||||
|
||||
private IFontHandleManager ManagerNotDisposed =>
|
||||
this.manager ?? throw new ObjectDisposedException(nameof(GamePrebakedFontHandle));
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Dispose()
|
||||
{
|
||||
if (this.pushedFonts.Count > 0)
|
||||
Log.Warning($"{nameof(IFontHandle)}.{nameof(IDisposable.Dispose)}: fonts were still in a stack.");
|
||||
this.manager?.FreeFontHandle(this);
|
||||
this.manager = null;
|
||||
this.Disposed?.InvokeSafely(this);
|
||||
this.ImFontChanged = null;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IFontHandle.ImFontLocked Lock()
|
||||
{
|
||||
IFontHandleSubstance? prevSubstance = default;
|
||||
while (true)
|
||||
{
|
||||
var substance = this.ManagerNotDisposed.Substance;
|
||||
if (substance is null)
|
||||
throw new InvalidOperationException();
|
||||
if (substance == prevSubstance)
|
||||
throw new ObjectDisposedException(nameof(DelegateFontHandle));
|
||||
|
||||
prevSubstance = substance;
|
||||
try
|
||||
{
|
||||
substance.DataRoot.AddRef();
|
||||
}
|
||||
catch (ObjectDisposedException)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var fontPtr = substance.GetFontPtr(this);
|
||||
if (fontPtr.IsNull())
|
||||
continue;
|
||||
return new(fontPtr, substance.DataRoot);
|
||||
}
|
||||
finally
|
||||
{
|
||||
substance.DataRoot.Release();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IDisposable Push()
|
||||
{
|
||||
ThreadSafety.AssertMainThread();
|
||||
var cumulativePresentCalls = Service<InterfaceManager>.GetNullable()?.CumulativePresentCalls ?? 0L;
|
||||
if (this.lastCumulativePresentCalls != cumulativePresentCalls)
|
||||
{
|
||||
this.lastCumulativePresentCalls = cumulativePresentCalls;
|
||||
if (this.pushedFonts.Count > 0)
|
||||
{
|
||||
Log.Warning(
|
||||
$"{nameof(this.Push)} has been called, but the handle-private stack was not empty. " +
|
||||
$"You might be missing a call to {nameof(this.Pop)}.");
|
||||
this.pushedFonts.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
var rented = SimplePushedFont.Rent(this.pushedFonts, this.ImFont, this.Available);
|
||||
this.pushedFonts.Add(rented);
|
||||
return rented;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Pop()
|
||||
{
|
||||
ThreadSafety.AssertMainThread();
|
||||
this.pushedFonts[^1].Dispose();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task<IFontHandle> WaitAsync()
|
||||
{
|
||||
if (this.Available)
|
||||
return Task.FromResult<IFontHandle>(this);
|
||||
|
||||
var tcs = new TaskCompletionSource<IFontHandle>();
|
||||
this.ImFontChanged += OnImFontChanged;
|
||||
this.Disposed += OnImFontChanged;
|
||||
if (this.Available)
|
||||
OnImFontChanged(this);
|
||||
return tcs.Task;
|
||||
|
||||
void OnImFontChanged(IFontHandle unused)
|
||||
{
|
||||
if (tcs.Task.IsCompletedSuccessfully)
|
||||
return;
|
||||
|
||||
this.ImFontChanged -= OnImFontChanged;
|
||||
this.Disposed -= OnImFontChanged;
|
||||
if (this.manager is null)
|
||||
tcs.SetException(new ObjectDisposedException(nameof(GamePrebakedFontHandle)));
|
||||
else
|
||||
tcs.SetResult(this);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Manager for <see cref="DelegateFontHandle"/>s.
|
||||
/// </summary>
|
||||
|
|
@ -209,16 +80,6 @@ internal class DelegateFontHandle : IFontHandle.IInternal
|
|||
this.handles.Remove(cgfh);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
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)
|
||||
{
|
||||
|
|
@ -262,6 +123,9 @@ internal class DelegateFontHandle : IFontHandle.IInternal
|
|||
// Not owned by this class. Do not dispose.
|
||||
public DelegateFontHandle[] RelevantHandles { get; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
ICollection<FontHandle> IFontHandleSubstance.RelevantHandles => this.RelevantHandles;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IRefCountable DataRoot { get; }
|
||||
|
||||
|
|
@ -435,32 +299,5 @@ internal class DelegateFontHandle : IFontHandle.IInternal
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void OnPostPromotion(IFontAtlasBuildToolkitPostPromotion toolkitPostPromotion)
|
||||
{
|
||||
foreach (var k in this.RelevantHandles)
|
||||
{
|
||||
if (!this.fonts[k].IsNotNullAndLoaded())
|
||||
continue;
|
||||
|
||||
try
|
||||
{
|
||||
toolkitPostPromotion.Font = this.fonts[k];
|
||||
k.CallOnBuildStepChange.Invoke(toolkitPostPromotion);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
this.fonts[k] = default;
|
||||
this.buildExceptions[k] = e;
|
||||
|
||||
Log.Error(
|
||||
e,
|
||||
"[{name}:Substance] An error has occurred while during {delegate} PostPromotion call.",
|
||||
this.Manager.Name,
|
||||
nameof(FontAtlasBuildStepDelegate));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -135,6 +135,19 @@ internal sealed partial class FontAtlasFactory
|
|||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ImFontPtr GetFont(IFontHandle fontHandle)
|
||||
{
|
||||
foreach (var s in this.data.Substances)
|
||||
{
|
||||
var f = s.GetFontPtr(fontHandle);
|
||||
if (!f.IsNull())
|
||||
return f;
|
||||
}
|
||||
|
||||
return default;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ImFontPtr IgnoreGlobalScale(ImFontPtr fontPtr)
|
||||
{
|
||||
|
|
@ -608,49 +621,6 @@ internal sealed partial class FontAtlasFactory
|
|||
ArrayPool<byte>.Shared.Return(buf);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Implementations for <see cref="IFontAtlasBuildToolkitPostPromotion"/>.
|
||||
/// </summary>
|
||||
private class BuildToolkitPostPromotion : IFontAtlasBuildToolkitPostPromotion
|
||||
{
|
||||
private readonly FontAtlasBuiltData builtData;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="BuildToolkitPostPromotion"/> class.
|
||||
/// </summary>
|
||||
/// <param name="builtData">The built data.</param>
|
||||
public BuildToolkitPostPromotion(FontAtlasBuiltData builtData) => this.builtData = builtData;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ImFontPtr Font { get; set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public float Scale => this.builtData.Scale;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsAsyncBuildOperation => true;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public FontAtlasBuildStep BuildStep => FontAtlasBuildStep.PostPromotion;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ImFontAtlasPtr NewImAtlas => this.builtData.Atlas;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public unsafe ImVectorWrapper<ImFontPtr> Fonts => new(
|
||||
&this.NewImAtlas.NativePtr->Fonts,
|
||||
x => ImGuiNative.ImFont_destroy(x->NativePtr));
|
||||
|
||||
/// <inheritdoc/>
|
||||
public T DisposeWithAtlas<T>(T disposable) where T : IDisposable => this.builtData.Garbage.Add(disposable);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public GCHandle DisposeWithAtlas(GCHandle gcHandle) => this.builtData.Garbage.Add(gcHandle);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void DisposeWithAtlas(Action action) => this.builtData.Garbage.Add(action);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public unsafe void CopyGlyphsAcrossFonts(
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ using System.Collections.Generic;
|
|||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Reactive.Disposables;
|
||||
using System.Runtime.ExceptionServices;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
|
|
@ -168,7 +169,7 @@ internal sealed partial class FontAtlasFactory
|
|||
_ => throw new InvalidOperationException(),
|
||||
};
|
||||
|
||||
public unsafe int Release()
|
||||
public int Release()
|
||||
{
|
||||
switch (IRefCountable.AlterRefCount(-1, ref this.refCount, out var newRefCount))
|
||||
{
|
||||
|
|
@ -176,22 +177,35 @@ internal sealed partial class FontAtlasFactory
|
|||
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);
|
||||
#endif
|
||||
this.Garbage.Dispose();
|
||||
|
||||
if (this.IsBuildInProgress)
|
||||
{
|
||||
unsafe
|
||||
{
|
||||
Log.Error(
|
||||
"[{name}] 0x{ptr:X}: Trying to dispose while build is in progress; disposing later.\n" +
|
||||
"Stack:\n{trace}",
|
||||
this.Owner?.Name ?? "<?>",
|
||||
(nint)this.Atlas.NativePtr,
|
||||
new StackTrace());
|
||||
}
|
||||
|
||||
Task.Run(
|
||||
async () =>
|
||||
{
|
||||
while (this.IsBuildInProgress)
|
||||
await Task.Delay(100);
|
||||
this.Garbage.Dispose();
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
this.Garbage.Dispose();
|
||||
}
|
||||
|
||||
return newRefCount;
|
||||
|
||||
case IRefCountable.RefCountResult.AlreadyDisposed:
|
||||
|
|
@ -217,7 +231,6 @@ internal sealed partial class FontAtlasFactory
|
|||
private readonly GamePrebakedFontHandle.HandleManager gameFontHandleManager;
|
||||
private readonly IFontHandleManager[] fontHandleManagers;
|
||||
|
||||
private readonly object syncRootPostPromotion = new();
|
||||
private readonly object syncRoot = new();
|
||||
|
||||
private Task<FontAtlasBuiltData?> buildTask = EmptyTask;
|
||||
|
|
@ -437,10 +450,9 @@ internal sealed partial class FontAtlasFactory
|
|||
}
|
||||
|
||||
var tcs = new TaskCompletionSource<FontAtlasBuiltData>();
|
||||
int rebuildIndex;
|
||||
try
|
||||
{
|
||||
rebuildIndex = ++this.buildIndex;
|
||||
var rebuildIndex = Interlocked.Increment(ref this.buildIndex);
|
||||
lock (this.syncRoot)
|
||||
{
|
||||
if (!this.buildTask.IsCompleted)
|
||||
|
|
@ -457,11 +469,18 @@ internal sealed partial class FontAtlasFactory
|
|||
var r = this.RebuildFontsPrivate(false, scale);
|
||||
r.Wait();
|
||||
if (r.IsCompletedSuccessfully)
|
||||
{
|
||||
this.PromoteBuiltData(rebuildIndex, r.Result, nameof(this.BuildFontsImmediately));
|
||||
tcs.SetResult(r.Result);
|
||||
else if (r.Exception is not null)
|
||||
tcs.SetException(r.Exception);
|
||||
}
|
||||
else if ((r.Exception?.InnerException ?? r.Exception) is { } taskException)
|
||||
{
|
||||
ExceptionDispatchInfo.Capture(taskException).Throw();
|
||||
}
|
||||
else
|
||||
tcs.SetCanceled();
|
||||
{
|
||||
throw new OperationCanceledException();
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
|
|
@ -469,12 +488,10 @@ internal sealed partial class FontAtlasFactory
|
|||
Log.Error(e, "[{name}] Failed to build fonts.", this.Name);
|
||||
throw;
|
||||
}
|
||||
|
||||
this.InvokePostPromotion(rebuildIndex, tcs.Task.Result, nameof(this.BuildFontsImmediately));
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task BuildFontsAsync(bool callPostPromotionOnMainThread = true)
|
||||
public Task BuildFontsAsync()
|
||||
{
|
||||
#if VeryVerboseLog
|
||||
Log.Verbose("[{name}] Called: {source}.", this.Name, nameof(this.BuildFontsAsync));
|
||||
|
|
@ -491,7 +508,7 @@ internal sealed partial class FontAtlasFactory
|
|||
lock (this.syncRoot)
|
||||
{
|
||||
var scale = this.IsGlobalScaled ? ImGuiHelpers.GlobalScaleSafe : 1f;
|
||||
var rebuildIndex = ++this.buildIndex;
|
||||
var rebuildIndex = Interlocked.Increment(ref this.buildIndex);
|
||||
return this.buildTask = this.buildTask.ContinueWith(BuildInner).Unwrap();
|
||||
|
||||
async Task<FontAtlasBuiltData?> BuildInner(Task<FontAtlasBuiltData> unused)
|
||||
|
|
@ -507,23 +524,19 @@ internal sealed partial class FontAtlasFactory
|
|||
if (res.Atlas.IsNull())
|
||||
return res;
|
||||
|
||||
if (callPostPromotionOnMainThread)
|
||||
{
|
||||
await this.factory.Framework.RunOnFrameworkThread(
|
||||
() => this.InvokePostPromotion(rebuildIndex, res, nameof(this.BuildFontsAsync)));
|
||||
}
|
||||
else
|
||||
{
|
||||
this.InvokePostPromotion(rebuildIndex, res, nameof(this.BuildFontsAsync));
|
||||
}
|
||||
this.PromoteBuiltData(rebuildIndex, res, nameof(this.BuildFontsAsync));
|
||||
|
||||
return res;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void InvokePostPromotion(int rebuildIndex, FontAtlasBuiltData data, [UsedImplicitly] string source)
|
||||
private void PromoteBuiltData(int rebuildIndex, FontAtlasBuiltData data, [UsedImplicitly] string source)
|
||||
{
|
||||
// Capture the locks inside the lock block, so that the fonts are guaranteed to be the ones just built.
|
||||
var fontsAndLocks = new List<(FontHandle FontHandle, ILockedImFont Lock)>();
|
||||
using var garbage = new DisposeSafety.ScopedFinalizer();
|
||||
|
||||
lock (this.syncRoot)
|
||||
{
|
||||
if (this.buildIndex != rebuildIndex)
|
||||
|
|
@ -537,68 +550,27 @@ internal sealed partial class FontAtlasFactory
|
|||
prevBuiltData.ExplicitDisposeIgnoreExceptions();
|
||||
|
||||
this.buildTask = EmptyTask;
|
||||
fontsAndLocks.EnsureCapacity(data.Substances.Sum(x => x.RelevantHandles.Count));
|
||||
foreach (var substance in data.Substances)
|
||||
{
|
||||
substance.Manager.Substance = substance;
|
||||
foreach (var fontHandle in substance.RelevantHandles)
|
||||
{
|
||||
substance.DataRoot.AddRef();
|
||||
var locked = new LockedImFont(
|
||||
substance.GetFontPtr(fontHandle),
|
||||
substance.DataRoot);
|
||||
fontsAndLocks.Add((fontHandle, garbage.Add(locked)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
lock (this.syncRootPostPromotion)
|
||||
{
|
||||
if (this.buildIndex != rebuildIndex)
|
||||
{
|
||||
data.ExplicitDisposeIgnoreExceptions();
|
||||
return;
|
||||
}
|
||||
|
||||
var toolkit = new BuildToolkitPostPromotion(data);
|
||||
|
||||
try
|
||||
{
|
||||
this.BuildStepChange?.Invoke(toolkit);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(
|
||||
e,
|
||||
"[{name}] {delegateName} PostPromotion error",
|
||||
this.Name,
|
||||
nameof(FontAtlasBuildStepDelegate));
|
||||
}
|
||||
|
||||
foreach (var substance in data.Substances)
|
||||
{
|
||||
try
|
||||
{
|
||||
substance.OnPostPromotion(toolkit);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(
|
||||
e,
|
||||
"[{name}] {substance} PostPromotion error",
|
||||
this.Name,
|
||||
substance.GetType().FullName ?? substance.GetType().Name);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var font in toolkit.Fonts)
|
||||
{
|
||||
try
|
||||
{
|
||||
toolkit.BuildLookupTable(font);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e, "[{name}] BuildLookupTable error", this.Name);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var substance in data.Substances)
|
||||
substance.Manager.InvokeFontHandleImFontChanged();
|
||||
foreach (var (fontHandle, lockedFont) in fontsAndLocks)
|
||||
fontHandle.InvokeImFontChanged(lockedFont);
|
||||
|
||||
#if VeryVerboseLog
|
||||
Log.Verbose("[{name}] Built from {source}.", this.Name, source);
|
||||
Log.Verbose("[{name}] Built from {source}.", this.Name, source);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
private void ImGuiSceneOnNewRenderFrame()
|
||||
|
|
@ -709,6 +681,9 @@ internal sealed partial class FontAtlasFactory
|
|||
toolkit.PostBuildSubstances();
|
||||
this.BuildStepChange?.Invoke(toolkit);
|
||||
|
||||
foreach (var font in toolkit.Fonts)
|
||||
toolkit.BuildLookupTable(font);
|
||||
|
||||
if (this.factory.SceneTask is { IsCompleted: false } sceneTask)
|
||||
{
|
||||
Log.Verbose(
|
||||
|
|
@ -754,6 +729,8 @@ internal sealed partial class FontAtlasFactory
|
|||
}
|
||||
finally
|
||||
{
|
||||
// RS is being dumb
|
||||
// ReSharper disable once ConstantConditionalAccessQualifier
|
||||
toolkit?.Dispose();
|
||||
this.buildQueued = false;
|
||||
}
|
||||
|
|
|
|||
295
Dalamud/Interface/ManagedFontAtlas/Internals/FontHandle.cs
Normal file
295
Dalamud/Interface/ManagedFontAtlas/Internals/FontHandle.cs
Normal file
|
|
@ -0,0 +1,295 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Dalamud.Interface.Internal;
|
||||
using Dalamud.Interface.Utility;
|
||||
using Dalamud.Plugin.Internal;
|
||||
using Dalamud.Plugin.Internal.Types;
|
||||
using Dalamud.Utility;
|
||||
|
||||
using ImGuiNET;
|
||||
|
||||
using Serilog;
|
||||
|
||||
namespace Dalamud.Interface.ManagedFontAtlas.Internals;
|
||||
|
||||
/// <summary>
|
||||
/// Default implementation for <see cref="FontHandle"/>.
|
||||
/// </summary>
|
||||
internal abstract class FontHandle : IFontHandle
|
||||
{
|
||||
private const int NonMainThreadFontAccessWarningCheckInterval = 10000;
|
||||
private static readonly ConditionalWeakTable<LocalPlugin, object> NonMainThreadFontAccessWarning = new();
|
||||
private static long nextNonMainThreadFontAccessWarningCheck;
|
||||
|
||||
private readonly InterfaceManager interfaceManager;
|
||||
private readonly List<IDisposable> pushedFonts = new(8);
|
||||
|
||||
private IFontHandleManager? manager;
|
||||
private long lastCumulativePresentCalls;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="FontHandle"/> class.
|
||||
/// </summary>
|
||||
/// <param name="manager">An instance of <see cref="IFontHandleManager"/>.</param>
|
||||
protected FontHandle(IFontHandleManager manager)
|
||||
{
|
||||
this.interfaceManager = Service<InterfaceManager>.Get();
|
||||
this.manager = manager;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public event IFontHandle.ImFontChangedDelegate? ImFontChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Event to be called on the first <see cref="IDisposable.Dispose"/> call.
|
||||
/// </summary>
|
||||
protected event Action? Disposed;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Exception? LoadException => this.Manager.Substance?.GetBuildException(this);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool Available => (this.Manager.Substance?.GetFontPtr(this) ?? default).IsNotNullAndLoaded();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the associated <see cref="IFontHandleManager"/>.
|
||||
/// </summary>
|
||||
/// <exception cref="ObjectDisposedException">When the object has already been disposed.</exception>
|
||||
protected IFontHandleManager Manager => this.manager ?? throw new ObjectDisposedException(this.GetType().Name);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Dispose()
|
||||
{
|
||||
if (this.manager is null)
|
||||
return;
|
||||
|
||||
this.Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invokes <see cref="IFontHandle.ImFontChanged"/>.
|
||||
/// </summary>
|
||||
/// <param name="font">The font, locked during the call of <see cref="ImFontChanged"/>.</param>
|
||||
public void InvokeImFontChanged(ILockedImFont font)
|
||||
{
|
||||
try
|
||||
{
|
||||
this.ImFontChanged?.Invoke(this, font);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e, $"{nameof(this.InvokeImFontChanged)}: error");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Obtains an instance of <see cref="ImFontPtr"/> corresponding to this font handle,
|
||||
/// to be released after rendering the current frame.
|
||||
/// </summary>
|
||||
/// <returns>The font pointer, or default if unavailble.</returns>
|
||||
/// <remarks>
|
||||
/// Behavior is undefined on access outside the main thread.
|
||||
/// </remarks>
|
||||
public ImFontPtr LockUntilPostFrame()
|
||||
{
|
||||
if (this.TryLock(out _) is not { } locked)
|
||||
return default;
|
||||
|
||||
if (!ThreadSafety.IsMainThread && nextNonMainThreadFontAccessWarningCheck < Environment.TickCount64)
|
||||
{
|
||||
nextNonMainThreadFontAccessWarningCheck =
|
||||
Environment.TickCount64 + NonMainThreadFontAccessWarningCheckInterval;
|
||||
var stack = new StackTrace();
|
||||
if (Service<PluginManager>.GetNullable()?.FindCallingPlugin(stack) is { } plugin)
|
||||
{
|
||||
if (!NonMainThreadFontAccessWarning.TryGetValue(plugin, out _))
|
||||
{
|
||||
NonMainThreadFontAccessWarning.Add(plugin, new());
|
||||
Log.Warning(
|
||||
"[IM] {pluginName}: Accessing fonts outside the main thread is deprecated.\n{stack}",
|
||||
plugin.Name,
|
||||
stack);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Dalamud internal should be made safe right now
|
||||
throw new InvalidOperationException("Attempted to access fonts outside the main thread.");
|
||||
}
|
||||
}
|
||||
|
||||
this.interfaceManager.EnqueueDeferredDispose(locked);
|
||||
return locked.ImFont;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to lock the fully constructed instance of <see cref="ImFontPtr"/> corresponding to the this
|
||||
/// <see cref="IFontHandle"/>, for use in any thread.<br />
|
||||
/// Modification of the font will exhibit undefined behavior if some other thread also uses the font.
|
||||
/// </summary>
|
||||
/// <param name="errorMessage">The error message, if any.</param>
|
||||
/// <returns>
|
||||
/// An instance of <see cref="ILockedImFont"/> that <b>must</b> be disposed after use on success;
|
||||
/// <c>null</c> with <paramref name="errorMessage"/> populated on failure.
|
||||
/// </returns>
|
||||
/// <exception cref="ObjectDisposedException">Still may be thrown.</exception>
|
||||
public ILockedImFont? TryLock(out string? errorMessage)
|
||||
{
|
||||
IFontHandleSubstance? prevSubstance = default;
|
||||
while (true)
|
||||
{
|
||||
var substance = this.Manager.Substance;
|
||||
|
||||
// Does the associated IFontAtlas have a built substance?
|
||||
if (substance is null)
|
||||
{
|
||||
errorMessage = "The font atlas has not been built yet.";
|
||||
return null;
|
||||
}
|
||||
|
||||
// Did we loop (because it did not have the requested font),
|
||||
// and are the fetched substance same between loops?
|
||||
if (substance == prevSubstance)
|
||||
{
|
||||
errorMessage = "The font atlas did not built the requested handle yet.";
|
||||
return null;
|
||||
}
|
||||
|
||||
prevSubstance = substance;
|
||||
|
||||
// Try to lock the substance.
|
||||
try
|
||||
{
|
||||
substance.DataRoot.AddRef();
|
||||
}
|
||||
catch (ObjectDisposedException)
|
||||
{
|
||||
// If it got invalidated, it's probably because a new substance is incoming. Try again.
|
||||
continue;
|
||||
}
|
||||
|
||||
var fontPtr = substance.GetFontPtr(this);
|
||||
if (fontPtr.IsNull())
|
||||
{
|
||||
// The font for the requested handle is unavailable. Release the reference and try again.
|
||||
substance.DataRoot.Release();
|
||||
continue;
|
||||
}
|
||||
|
||||
// Transfer the ownership of reference.
|
||||
errorMessage = null;
|
||||
return new LockedImFont(fontPtr, substance.DataRoot);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ILockedImFont Lock() =>
|
||||
this.TryLock(out var errorMessage) ?? throw new InvalidOperationException(errorMessage);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IDisposable Push()
|
||||
{
|
||||
ThreadSafety.AssertMainThread();
|
||||
|
||||
// Warn if the client is not properly managing the pushed font stack.
|
||||
var cumulativePresentCalls = this.interfaceManager.CumulativePresentCalls;
|
||||
if (this.lastCumulativePresentCalls != cumulativePresentCalls)
|
||||
{
|
||||
this.lastCumulativePresentCalls = cumulativePresentCalls;
|
||||
if (this.pushedFonts.Count > 0)
|
||||
{
|
||||
Log.Warning(
|
||||
$"{nameof(this.Push)} has been called, but the handle-private stack was not empty. " +
|
||||
$"You might be missing a call to {nameof(this.Pop)}.");
|
||||
this.pushedFonts.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
var font = default(ImFontPtr);
|
||||
if (this.TryLock(out _) is { } locked)
|
||||
{
|
||||
font = locked.ImFont;
|
||||
this.interfaceManager.EnqueueDeferredDispose(locked);
|
||||
}
|
||||
|
||||
var rented = SimplePushedFont.Rent(this.pushedFonts, font);
|
||||
this.pushedFonts.Add(rented);
|
||||
return rented;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Pop()
|
||||
{
|
||||
ThreadSafety.AssertMainThread();
|
||||
this.pushedFonts[^1].Dispose();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task<IFontHandle> WaitAsync()
|
||||
{
|
||||
if (this.Available)
|
||||
return Task.FromResult<IFontHandle>(this);
|
||||
|
||||
var tcs = new TaskCompletionSource<IFontHandle>();
|
||||
this.ImFontChanged += OnImFontChanged;
|
||||
this.Disposed += OnDisposed;
|
||||
if (this.Available)
|
||||
OnImFontChanged(this, null);
|
||||
return tcs.Task;
|
||||
|
||||
void OnImFontChanged(IFontHandle unused, ILockedImFont? unused2)
|
||||
{
|
||||
if (tcs.Task.IsCompletedSuccessfully)
|
||||
return;
|
||||
|
||||
this.ImFontChanged -= OnImFontChanged;
|
||||
this.Disposed -= OnDisposed;
|
||||
try
|
||||
{
|
||||
tcs.SetResult(this);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
void OnDisposed()
|
||||
{
|
||||
if (tcs.Task.IsCompletedSuccessfully)
|
||||
return;
|
||||
|
||||
this.ImFontChanged -= OnImFontChanged;
|
||||
this.Disposed -= OnDisposed;
|
||||
try
|
||||
{
|
||||
tcs.SetException(new ObjectDisposedException(nameof(GamePrebakedFontHandle)));
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Implementation for <see cref="IDisposable.Dispose"/>.
|
||||
/// </summary>
|
||||
/// <param name="disposing">If <c>true</c>, then the function is being called from <see cref="IDisposable.Dispose"/>.</param>
|
||||
protected void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
if (this.pushedFonts.Count > 0)
|
||||
Log.Warning($"{nameof(IFontHandle)}.{nameof(IDisposable.Dispose)}: fonts were still in a stack.");
|
||||
this.Manager.FreeFontHandle(this);
|
||||
this.manager = null;
|
||||
this.Disposed?.InvokeSafely();
|
||||
this.ImFontChanged = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -4,7 +4,6 @@ using System.Collections.Generic;
|
|||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using System.Reactive.Disposables;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Dalamud.Game.Text;
|
||||
using Dalamud.Interface.GameFonts;
|
||||
|
|
@ -16,8 +15,6 @@ using ImGuiNET;
|
|||
|
||||
using Lumina.Data.Files;
|
||||
|
||||
using Serilog;
|
||||
|
||||
using Vector4 = System.Numerics.Vector4;
|
||||
|
||||
namespace Dalamud.Interface.ManagedFontAtlas.Internals;
|
||||
|
|
@ -25,7 +22,7 @@ namespace Dalamud.Interface.ManagedFontAtlas.Internals;
|
|||
/// <summary>
|
||||
/// A font handle that uses the game's built-in fonts, optionally with some styling.
|
||||
/// </summary>
|
||||
internal class GamePrebakedFontHandle : IFontHandle.IInternal
|
||||
internal class GamePrebakedFontHandle : FontHandle
|
||||
{
|
||||
/// <summary>
|
||||
/// The smallest value of <see cref="SeIconChar"/>.
|
||||
|
|
@ -37,17 +34,13 @@ internal class GamePrebakedFontHandle : IFontHandle.IInternal
|
|||
/// </summary>
|
||||
public static readonly char SeIconCharMax = (char)Enum.GetValues<SeIconChar>().Max();
|
||||
|
||||
private readonly List<IDisposable> pushedFonts = new(8);
|
||||
|
||||
private IFontHandleManager? manager;
|
||||
private long lastCumulativePresentCalls;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="GamePrebakedFontHandle"/> class.
|
||||
/// </summary>
|
||||
/// <param name="manager">An instance of <see cref="IFontHandleManager"/>.</param>
|
||||
/// <param name="style">Font to use.</param>
|
||||
public GamePrebakedFontHandle(IFontHandleManager manager, GameFontStyle style)
|
||||
: base(manager)
|
||||
{
|
||||
if (!Enum.IsDefined(style.FamilyAndSize) || style.FamilyAndSize == GameFontFamilyAndSize.Undefined)
|
||||
throw new ArgumentOutOfRangeException(nameof(style), style, null);
|
||||
|
|
@ -55,15 +48,9 @@ internal class GamePrebakedFontHandle : IFontHandle.IInternal
|
|||
if (style.SizePt <= 0)
|
||||
throw new ArgumentException($"{nameof(style.SizePt)} must be a positive number.", nameof(style));
|
||||
|
||||
this.manager = manager;
|
||||
this.FontStyle = style;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public event Action<IFontHandle>? ImFontChanged;
|
||||
|
||||
private event Action<IFontHandle>? Disposed;
|
||||
|
||||
/// <summary>
|
||||
/// Provider for <see cref="IDalamudTextureWrap"/> for `common/font/fontNN.tex`.
|
||||
/// </summary>
|
||||
|
|
@ -107,119 +94,6 @@ internal class GamePrebakedFontHandle : IFontHandle.IInternal
|
|||
/// </summary>
|
||||
public GameFontStyle FontStyle { get; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Exception? LoadException => this.ManagerNotDisposed.Substance?.GetBuildException(this);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool Available => this.ImFont.IsNotNullAndLoaded();
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ImFontPtr ImFont => this.ManagerNotDisposed.Substance?.GetFontPtr(this) ?? default;
|
||||
|
||||
private IFontHandleManager ManagerNotDisposed =>
|
||||
this.manager ?? throw new ObjectDisposedException(nameof(GamePrebakedFontHandle));
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Dispose()
|
||||
{
|
||||
this.manager?.FreeFontHandle(this);
|
||||
this.manager = null;
|
||||
this.Disposed?.InvokeSafely(this);
|
||||
this.ImFontChanged = null;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IFontHandle.ImFontLocked Lock()
|
||||
{
|
||||
IFontHandleSubstance? prevSubstance = default;
|
||||
while (true)
|
||||
{
|
||||
var substance = this.ManagerNotDisposed.Substance;
|
||||
if (substance is null)
|
||||
throw new InvalidOperationException();
|
||||
if (substance == prevSubstance)
|
||||
throw new ObjectDisposedException(nameof(DelegateFontHandle));
|
||||
|
||||
prevSubstance = substance;
|
||||
try
|
||||
{
|
||||
substance.DataRoot.AddRef();
|
||||
}
|
||||
catch (ObjectDisposedException)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var fontPtr = substance.GetFontPtr(this);
|
||||
if (fontPtr.IsNull())
|
||||
continue;
|
||||
return new(fontPtr, substance.DataRoot);
|
||||
}
|
||||
finally
|
||||
{
|
||||
substance.DataRoot.Release();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IDisposable Push()
|
||||
{
|
||||
ThreadSafety.AssertMainThread();
|
||||
var cumulativePresentCalls = Service<InterfaceManager>.GetNullable()?.CumulativePresentCalls ?? 0L;
|
||||
if (this.lastCumulativePresentCalls != cumulativePresentCalls)
|
||||
{
|
||||
this.lastCumulativePresentCalls = cumulativePresentCalls;
|
||||
if (this.pushedFonts.Count > 0)
|
||||
{
|
||||
Log.Warning(
|
||||
$"{nameof(this.Push)} has been called, but the handle-private stack was not empty. " +
|
||||
$"You might be missing a call to {nameof(this.Pop)}.");
|
||||
this.pushedFonts.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
var rented = SimplePushedFont.Rent(this.pushedFonts, this.ImFont, this.Available);
|
||||
this.pushedFonts.Add(rented);
|
||||
return rented;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Pop()
|
||||
{
|
||||
ThreadSafety.AssertMainThread();
|
||||
this.pushedFonts[^1].Dispose();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task<IFontHandle> WaitAsync()
|
||||
{
|
||||
if (this.Available)
|
||||
return Task.FromResult<IFontHandle>(this);
|
||||
|
||||
var tcs = new TaskCompletionSource<IFontHandle>();
|
||||
this.ImFontChanged += OnImFontChanged;
|
||||
this.Disposed += OnImFontChanged;
|
||||
if (this.Available)
|
||||
OnImFontChanged(this);
|
||||
return tcs.Task;
|
||||
|
||||
void OnImFontChanged(IFontHandle unused)
|
||||
{
|
||||
if (tcs.Task.IsCompletedSuccessfully)
|
||||
return;
|
||||
|
||||
this.ImFontChanged -= OnImFontChanged;
|
||||
this.Disposed -= OnImFontChanged;
|
||||
if (this.manager is null)
|
||||
tcs.SetException(new ObjectDisposedException(nameof(GamePrebakedFontHandle)));
|
||||
else
|
||||
tcs.SetResult(this);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string ToString() => $"{nameof(GamePrebakedFontHandle)}({this.FontStyle})";
|
||||
|
||||
|
|
@ -298,16 +172,6 @@ internal class GamePrebakedFontHandle : IFontHandle.IInternal
|
|||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
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)
|
||||
{
|
||||
|
|
@ -358,6 +222,9 @@ internal class GamePrebakedFontHandle : IFontHandle.IInternal
|
|||
// Not owned by this class. Do not dispose.
|
||||
public GamePrebakedFontHandle[] RelevantHandles { get; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
ICollection<FontHandle> IFontHandleSubstance.RelevantHandles => this.RelevantHandles;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IRefCountable DataRoot { get; }
|
||||
|
||||
|
|
@ -539,12 +406,6 @@ internal class GamePrebakedFontHandle : IFontHandle.IInternal
|
|||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void OnPostPromotion(IFontAtlasBuildToolkitPostPromotion toolkitPostPromotion)
|
||||
{
|
||||
// Irrelevant
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new template font.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -32,9 +32,4 @@ internal interface IFontHandleManager : IDisposable
|
|||
/// <param name="dataRoot">The data root.</param>
|
||||
/// <returns>The new substance.</returns>
|
||||
IFontHandleSubstance NewSubstance(IRefCountable dataRoot);
|
||||
|
||||
/// <summary>
|
||||
/// Invokes <see cref="IFontHandle.ImFontChanged"/>.
|
||||
/// </summary>
|
||||
void InvokeFontHandleImFontChanged();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
using Dalamud.Utility;
|
||||
using System.Collections.Generic;
|
||||
|
||||
using Dalamud.Utility;
|
||||
|
||||
using ImGuiNET;
|
||||
|
||||
|
|
@ -32,6 +34,11 @@ internal interface IFontHandleSubstance : IDisposable
|
|||
[Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)]
|
||||
bool CreateFontOnAccess { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the relevant handles.
|
||||
/// </summary>
|
||||
public ICollection<FontHandle> RelevantHandles { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the font.
|
||||
/// </summary>
|
||||
|
|
@ -64,11 +71,4 @@ internal interface IFontHandleSubstance : IDisposable
|
|||
/// </summary>
|
||||
/// <param name="toolkitPostBuild">The toolkit.</param>
|
||||
void OnPostBuild(IFontAtlasBuildToolkitPostBuild toolkitPostBuild);
|
||||
|
||||
/// <summary>
|
||||
/// Called on the specific thread depending on <see cref="IFontAtlasBuildToolkit.IsAsyncBuildOperation"/> after
|
||||
/// promoting the staging atlas to direct use with <see cref="IFontAtlas"/>.
|
||||
/// </summary>
|
||||
/// <param name="toolkitPostPromotion">The toolkit.</param>
|
||||
void OnPostPromotion(IFontAtlasBuildToolkitPostPromotion toolkitPostPromotion);
|
||||
}
|
||||
|
|
|
|||
62
Dalamud/Interface/ManagedFontAtlas/Internals/LockedImFont.cs
Normal file
62
Dalamud/Interface/ManagedFontAtlas/Internals/LockedImFont.cs
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
using Dalamud.Utility;
|
||||
|
||||
using ImGuiNET;
|
||||
|
||||
namespace Dalamud.Interface.ManagedFontAtlas.Internals;
|
||||
|
||||
/// <summary>
|
||||
/// The implementation for <see cref="ILockedImFont"/>.
|
||||
/// </summary>
|
||||
internal class LockedImFont : ILockedImFont
|
||||
{
|
||||
private IRefCountable? owner;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="LockedImFont"/> class.
|
||||
/// Ownership of reference of <paramref name="owner"/> is transferred.
|
||||
/// </summary>
|
||||
/// <param name="font">The contained font.</param>
|
||||
/// <param name="owner">The owner.</param>
|
||||
/// <returns>The rented instance of <see cref="LockedImFont"/>.</returns>
|
||||
internal LockedImFont(ImFontPtr font, IRefCountable owner)
|
||||
{
|
||||
this.ImFont = font;
|
||||
this.owner = owner;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finalizes an instance of the <see cref="LockedImFont"/> class.
|
||||
/// </summary>
|
||||
~LockedImFont() => this.FreeOwner();
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ImFontPtr ImFont { get; private set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ILockedImFont NewRef()
|
||||
{
|
||||
if (this.owner is null)
|
||||
throw new ObjectDisposedException(nameof(LockedImFont));
|
||||
|
||||
var newRef = new LockedImFont(this.ImFont, this.owner);
|
||||
this.owner.AddRef();
|
||||
return newRef;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Dispose()
|
||||
{
|
||||
this.FreeOwner();
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
private void FreeOwner()
|
||||
{
|
||||
if (this.owner is null)
|
||||
return;
|
||||
|
||||
this.owner.Release();
|
||||
this.owner = null;
|
||||
this.ImFont = default;
|
||||
}
|
||||
}
|
||||
|
|
@ -28,17 +28,14 @@ internal sealed class SimplePushedFont : IDisposable
|
|||
/// </summary>
|
||||
/// <param name="stack">The <see cref="IFontHandle"/>-private stack.</param>
|
||||
/// <param name="fontPtr">The font pointer being pushed.</param>
|
||||
/// <param name="push">Whether to push.</param>
|
||||
/// <returns><c>this</c>.</returns>
|
||||
public static SimplePushedFont Rent(List<IDisposable> stack, ImFontPtr fontPtr, bool push)
|
||||
/// <returns>The rented instance of <see cref="SimplePushedFont"/>.</returns>
|
||||
public static SimplePushedFont Rent(List<IDisposable> stack, ImFontPtr fontPtr)
|
||||
{
|
||||
push &= !fontPtr.IsNull();
|
||||
|
||||
var rented = Pool.Get();
|
||||
Debug.Assert(rented.font.IsNull(), "Rented object must not have its font set");
|
||||
rented.stack = stack;
|
||||
|
||||
if (push)
|
||||
if (fontPtr.IsNotNullAndLoaded())
|
||||
{
|
||||
rented.font = fontPtr;
|
||||
ImGui.PushFont(fontPtr);
|
||||
|
|
|
|||
|
|
@ -498,7 +498,7 @@ public sealed class UiBuilder : IDisposable
|
|||
[Obsolete($"Use {nameof(this.FontAtlas)}.{nameof(IFontAtlas.NewGameFontHandle)} instead.", false)]
|
||||
[Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)]
|
||||
public GameFontHandle GetGameFontHandle(GameFontStyle style) => new(
|
||||
(IFontHandle.IInternal)this.FontAtlas.NewGameFontHandle(style),
|
||||
(GamePrebakedFontHandle)this.FontAtlas.NewGameFontHandle(style),
|
||||
Service<FontAtlasFactory>.Get());
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -700,6 +700,8 @@ public sealed class UiBuilder : IDisposable
|
|||
if (e.IsAsyncBuildOperation)
|
||||
return;
|
||||
|
||||
ThreadSafety.AssertMainThread();
|
||||
|
||||
if (this.BuildFonts is not null)
|
||||
{
|
||||
e.OnPreBuild(
|
||||
|
|
@ -742,7 +744,7 @@ public sealed class UiBuilder : IDisposable
|
|||
this.wrapped.ImFontChanged += this.WrappedOnImFontChanged;
|
||||
}
|
||||
|
||||
public event Action<IFontHandle>? ImFontChanged;
|
||||
public event IFontHandle.ImFontChangedDelegate? ImFontChanged;
|
||||
|
||||
public Exception? LoadException => this.WrappedNotDisposed.LoadException;
|
||||
|
||||
|
|
@ -761,7 +763,8 @@ public sealed class UiBuilder : IDisposable
|
|||
// Note: do not dispose w; we do not own it
|
||||
}
|
||||
|
||||
public IFontHandle.ImFontLocked Lock() => this.WrappedNotDisposed.Lock();
|
||||
public ILockedImFont Lock() =>
|
||||
this.wrapped?.Lock() ?? throw new ObjectDisposedException(nameof(FontHandleWrapper));
|
||||
|
||||
public IDisposable Push() => this.WrappedNotDisposed.Push();
|
||||
|
||||
|
|
@ -773,6 +776,7 @@ public sealed class UiBuilder : IDisposable
|
|||
public override string ToString() =>
|
||||
$"{nameof(FontHandleWrapper)}({this.wrapped?.ToString() ?? "disposed"})";
|
||||
|
||||
private void WrappedOnImFontChanged(IFontHandle obj) => this.ImFontChanged.InvokeSafely(this);
|
||||
private void WrappedOnImFontChanged(IFontHandle obj, ILockedImFont lockedFont) =>
|
||||
this.ImFontChanged?.Invoke(obj, lockedFont);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue