mirror of
https://github.com/goatcorp/Dalamud.git
synced 2026-01-01 05:13:40 +01:00
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:
parent
e20daed848
commit
5479149e79
8 changed files with 331 additions and 347 deletions
263
Dalamud/Interface/ManagedFontAtlas/Internals/FontHandle.cs
Normal file
263
Dalamud/Interface/ManagedFontAtlas/Internals/FontHandle.cs
Normal file
|
|
@ -0,0 +1,263 @@
|
|||
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 Action<IFontHandle>? ImFontChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Event to be called on the first <see cref="IDisposable.Dispose"/> call.
|
||||
/// </summary>
|
||||
protected event Action<IFontHandle>? 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>
|
||||
/// 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="IFontHandle.ImFontLocked"/> 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 IFontHandle.ImFontLocked? 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(fontPtr, substance.DataRoot);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IFontHandle.ImFontLocked 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 += 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>
|
||||
/// Invokes <see cref="IFontHandle.ImFontChanged"/>.
|
||||
/// </summary>
|
||||
protected void InvokeImFontChanged() => this.ImFontChanged.InvokeSafely(this);
|
||||
|
||||
/// <summary>
|
||||
/// Overrideable 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 virtual 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);
|
||||
this.ImFontChanged = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue