Lock font resources on Push and miscellaneous direct accesses

These changes ensure that using a font under some other thread's ownership from the UI thread for rendering into ImGui purposes always work.

* `FontHandle`:
    * Moved common code from `DelegateFontHandle` and `GamePrebakedFontHandle`.
    * Added `LockUntilPostFrame` so that the obtained `ImFontPtr` and its accompanying resources are kept valid until everything is rendered.
    * Added more code comments to `Try/Lock`.
    * Moved font access thread checking logic from `InterfaceManager` to `LockUntilPostFrame`.
    * `Push`ing a font will now also perform `LockUntilPostFrame`.
* `GameFontHandle`: Make the property `ImFont` a forwarder to `FontHandle.LockUntilPostFrame`.
* `InterfaceManager`:
    * Added companion logic to `FontHandle.LockUntilPostFrame`.
    * Accessing default/icon/mono fonts will forward to `FontHandle.LockUntilPostFrame`.
    * Changed `List<T>` to `ConcurrentBag<T>` as texture disposal can be done outside the main thread, and a race condition is possible.
This commit is contained in:
Soreepeong 2024-01-23 20:51:29 +09:00
parent e20daed848
commit 5479149e79
8 changed files with 331 additions and 347 deletions

View file

@ -1,3 +1,4 @@
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
@ -20,8 +21,6 @@ using Dalamud.Interface.ManagedFontAtlas.Internals;
using Dalamud.Interface.Style;
using Dalamud.Interface.Utility;
using Dalamud.Interface.Windowing;
using Dalamud.Plugin.Internal;
using Dalamud.Plugin.Internal.Types;
using Dalamud.Utility;
using Dalamud.Utility.Timing;
using ImGuiNET;
@ -63,15 +62,9 @@ internal class InterfaceManager : IDisposable, IServiceType
/// </summary>
public const float DefaultFontSizePx = (DefaultFontSizePt * 4.0f) / 3.0f;
private const int NonMainThreadFontAccessWarningCheckInterval = 10000;
private static readonly ConditionalWeakTable<LocalPlugin, object> NonMainThreadFontAccessWarning = new();
private static long nextNonMainThreadFontAccessWarningCheck;
private readonly ConcurrentBag<DalamudTextureWrap> deferredDisposeTextures = new();
private readonly ConcurrentBag<IFontHandle.ImFontLocked> deferredDisposeImFontLockeds = new();
private readonly List<DalamudTextureWrap> deferredDisposeTextures = new();
[ServiceManager.ServiceDependency]
private readonly Framework framework = Service<Framework>.Get();
[ServiceManager.ServiceDependency]
private readonly WndProcHookManager wndProcHookManager = Service<WndProcHookManager>.Get();
@ -127,34 +120,37 @@ internal class InterfaceManager : IDisposable, IServiceType
/// Gets the default ImGui font.<br />
/// <strong>Accessing this static property outside of the main thread is dangerous and not supported.</strong>
/// </summary>
public static ImFontPtr DefaultFont => WhenFontsReady().DefaultFontHandle!.ImFont.OrElse(ImGui.GetIO().FontDefault);
public static ImFontPtr DefaultFont =>
WhenFontsReady().DefaultFontHandle!.LockUntilPostFrame().OrElse(ImGui.GetIO().FontDefault);
/// <summary>
/// Gets an included FontAwesome icon font.<br />
/// <strong>Accessing this static property outside of the main thread is dangerous and not supported.</strong>
/// </summary>
public static ImFontPtr IconFont => WhenFontsReady().IconFontHandle!.ImFont.OrElse(ImGui.GetIO().FontDefault);
public static ImFontPtr IconFont =>
WhenFontsReady().IconFontHandle!.LockUntilPostFrame().OrElse(ImGui.GetIO().FontDefault);
/// <summary>
/// Gets an included monospaced font.<br />
/// <strong>Accessing this static property outside of the main thread is dangerous and not supported.</strong>
/// </summary>
public static ImFontPtr MonoFont => WhenFontsReady().MonoFontHandle!.ImFont.OrElse(ImGui.GetIO().FontDefault);
public static ImFontPtr MonoFont =>
WhenFontsReady().MonoFontHandle!.LockUntilPostFrame().OrElse(ImGui.GetIO().FontDefault);
/// <summary>
/// Gets the default font handle.
/// </summary>
public IFontHandle.IInternal? DefaultFontHandle { get; private set; }
public FontHandle? DefaultFontHandle { get; private set; }
/// <summary>
/// Gets the icon font handle.
/// </summary>
public IFontHandle.IInternal? IconFontHandle { get; private set; }
public FontHandle? IconFontHandle { get; private set; }
/// <summary>
/// Gets the mono font handle.
/// </summary>
public IFontHandle.IInternal? MonoFontHandle { get; private set; }
public FontHandle? MonoFontHandle { get; private set; }
/// <summary>
/// Gets or sets the pointer to ImGui.IO(), when it was last used.
@ -408,6 +404,15 @@ internal class InterfaceManager : IDisposable, IServiceType
this.deferredDisposeTextures.Add(wrap);
}
/// <summary>
/// Enqueue an <see cref="IFontHandle.ImFontLocked"/> to be disposed at the end of the frame.
/// </summary>
/// <param name="locked">The disposable.</param>
public void EnqueueDeferredDispose(in IFontHandle.ImFontLocked locked)
{
this.deferredDisposeImFontLockeds.Add(locked);
}
/// <summary>
/// Get video memory information.
/// </summary>
@ -466,29 +471,6 @@ internal class InterfaceManager : IDisposable, IServiceType
if (im?.dalamudAtlas is not { } atlas)
throw new InvalidOperationException($"Tried to access fonts before {nameof(ContinueConstruction)} call.");
if (!ThreadSafety.IsMainThread && nextNonMainThreadFontAccessWarningCheck < Environment.TickCount64)
{
nextNonMainThreadFontAccessWarningCheck =
Environment.TickCount64 + NonMainThreadFontAccessWarningCheckInterval;
var stack = new StackTrace();
if (Service<PluginManager>.GetNullable()?.FindCallingPlugin(stack) is { } plugin)
{
if (!NonMainThreadFontAccessWarning.TryGetValue(plugin, out _))
{
NonMainThreadFontAccessWarning.Add(plugin, new());
Log.Warning(
"[IM] {pluginName}: Accessing fonts outside the main thread is deprecated.\n{stack}",
plugin.Name,
stack);
}
}
else
{
// Dalamud internal should be made safe right now
throw new InvalidOperationException("Attempted to access fonts outside the main thread.");
}
}
if (!atlas.HasBuiltAtlas)
atlas.BuildTask.GetAwaiter().GetResult();
return im;
@ -673,28 +655,38 @@ internal class InterfaceManager : IDisposable, IServiceType
var pRes = this.presentHook!.Original(swapChain, syncInterval, presentFlags);
RenderImGui(this.scene!);
this.DisposeTextures();
this.CleanupPostImGuiRender();
return pRes;
}
RenderImGui(this.scene!);
this.DisposeTextures();
this.CleanupPostImGuiRender();
return this.presentHook!.Original(swapChain, syncInterval, presentFlags);
}
private void DisposeTextures()
private void CleanupPostImGuiRender()
{
if (this.deferredDisposeTextures.Count > 0)
if (!this.deferredDisposeTextures.IsEmpty)
{
Log.Verbose("[IM] Disposing {Count} textures", this.deferredDisposeTextures.Count);
foreach (var texture in this.deferredDisposeTextures)
var count = 0;
while (this.deferredDisposeTextures.TryTake(out var d))
{
texture.RealDispose();
count++;
d.RealDispose();
}
this.deferredDisposeTextures.Clear();
Log.Verbose("[IM] Disposing {Count} textures", count);
}
if (!this.deferredDisposeImFontLockeds.IsEmpty)
{
// Not logging; the main purpose of this is to keep resources used for rendering the frame to be kept
// referenced until the resources are actually done being used, and it is expected that this will be
// frequent.
while (this.deferredDisposeImFontLockeds.TryTake(out var d))
d.Dispose();
}
}
@ -709,9 +701,9 @@ internal class InterfaceManager : IDisposable, IServiceType
.CreateFontAtlas(nameof(InterfaceManager), FontAtlasAutoRebuildMode.Disable);
using (this.dalamudAtlas.SuppressAutoRebuild())
{
this.DefaultFontHandle = (IFontHandle.IInternal)this.dalamudAtlas.NewDelegateFontHandle(
this.DefaultFontHandle = (FontHandle)this.dalamudAtlas.NewDelegateFontHandle(
e => e.OnPreBuild(tk => tk.AddDalamudDefaultFont(DefaultFontSizePx)));
this.IconFontHandle = (IFontHandle.IInternal)this.dalamudAtlas.NewDelegateFontHandle(
this.IconFontHandle = (FontHandle)this.dalamudAtlas.NewDelegateFontHandle(
e => e.OnPreBuild(
tk => tk.AddFontAwesomeIconFont(
new()
@ -720,7 +712,7 @@ internal class InterfaceManager : IDisposable, IServiceType
GlyphMinAdvanceX = DefaultFontSizePx,
GlyphMaxAdvanceX = DefaultFontSizePx,
})));
this.MonoFontHandle = (IFontHandle.IInternal)this.dalamudAtlas.NewDelegateFontHandle(
this.MonoFontHandle = (FontHandle)this.dalamudAtlas.NewDelegateFontHandle(
e => e.OnPreBuild(
tk => tk.AddDalamudAssetFont(
DalamudAsset.InconsolataRegular,