mirror of
https://github.com/goatcorp/Dalamud.git
synced 2025-12-12 10:17:22 +01:00
`PostPromotion` is removed, as `IFontHandle.ImFontChanged` now does the job. It also removes the possibility that resources may get disposed while post promotion callback is in progress. * `IFontHandle.ImFontChanged` is now called with a locked instance of the font. * `IFontHandle.ImFontLocked`: Added `NewRef` to increase reference count.
303 lines
11 KiB
C#
303 lines
11 KiB
C#
using System.Collections.Generic;
|
|
using System.Linq;
|
|
|
|
using Dalamud.Interface.Utility;
|
|
using Dalamud.Logging.Internal;
|
|
using Dalamud.Utility;
|
|
|
|
using ImGuiNET;
|
|
|
|
namespace Dalamud.Interface.ManagedFontAtlas.Internals;
|
|
|
|
/// <summary>
|
|
/// A font handle representing a user-callback generated font.
|
|
/// </summary>
|
|
internal sealed class DelegateFontHandle : FontHandle
|
|
{
|
|
/// <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.CallOnBuildStepChange = callOnBuildStepChange;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the function to be called on build step changes.
|
|
/// </summary>
|
|
public FontAtlasBuildStepDelegate CallOnBuildStepChange { get; }
|
|
|
|
/// <summary>
|
|
/// Manager for <see cref="DelegateFontHandle"/>s.
|
|
/// </summary>
|
|
internal sealed class HandleManager : IFontHandleManager
|
|
{
|
|
private readonly HashSet<DelegateFontHandle> handles = new();
|
|
private readonly object syncRoot = new();
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="HandleManager"/> class.
|
|
/// </summary>
|
|
/// <param name="atlasName">The name of the owner atlas.</param>
|
|
public HandleManager(string atlasName) => this.Name = $"{atlasName}:{nameof(DelegateFontHandle)}:Manager";
|
|
|
|
/// <inheritdoc/>
|
|
public event Action? RebuildRecommend;
|
|
|
|
/// <inheritdoc/>
|
|
public string Name { get; }
|
|
|
|
/// <inheritdoc/>
|
|
public IFontHandleSubstance? Substance { get; set; }
|
|
|
|
/// <inheritdoc/>
|
|
public void Dispose()
|
|
{
|
|
lock (this.syncRoot)
|
|
this.handles.Clear();
|
|
}
|
|
|
|
/// <inheritdoc cref="IFontAtlas.NewDelegateFontHandle"/>
|
|
public IFontHandle NewFontHandle(FontAtlasBuildStepDelegate buildStepDelegate)
|
|
{
|
|
var key = new DelegateFontHandle(this, buildStepDelegate);
|
|
lock (this.syncRoot)
|
|
this.handles.Add(key);
|
|
this.RebuildRecommend?.Invoke();
|
|
return key;
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public void FreeFontHandle(IFontHandle handle)
|
|
{
|
|
if (handle is not DelegateFontHandle cgfh)
|
|
return;
|
|
|
|
lock (this.syncRoot)
|
|
this.handles.Remove(cgfh);
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public IFontHandleSubstance NewSubstance(IRefCountable dataRoot)
|
|
{
|
|
lock (this.syncRoot)
|
|
return new HandleSubstance(this, dataRoot, this.handles.ToArray());
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Substance from <see cref="HandleManager"/>.
|
|
/// </summary>
|
|
internal sealed class HandleSubstance : IFontHandleSubstance
|
|
{
|
|
private static readonly ModuleLog Log = new($"{nameof(DelegateFontHandle)}.{nameof(HandleSubstance)}");
|
|
|
|
// 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();
|
|
|
|
/// <summary>
|
|
/// 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,
|
|
IRefCountable dataRoot,
|
|
DelegateFontHandle[] relevantHandles)
|
|
{
|
|
// We do not call dataRoot.AddRef; this object is dependant on lifetime of dataRoot.
|
|
|
|
this.Manager = manager;
|
|
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/>
|
|
ICollection<FontHandle> IFontHandleSubstance.RelevantHandles => this.RelevantHandles;
|
|
|
|
/// <inheritdoc/>
|
|
public IRefCountable DataRoot { get; }
|
|
|
|
/// <inheritdoc/>
|
|
public IFontHandleManager Manager { get; }
|
|
|
|
/// <inheritdoc/>
|
|
[Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)]
|
|
public IFontAtlasBuildToolkitPreBuild? PreBuildToolkitForApi9Compat { get; set; }
|
|
|
|
/// <inheritdoc/>
|
|
[Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)]
|
|
public bool CreateFontOnAccess { get; set; }
|
|
|
|
/// <inheritdoc/>
|
|
public void Dispose()
|
|
{
|
|
this.fonts.Clear();
|
|
this.buildExceptions.Clear();
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public ImFontPtr GetFontPtr(IFontHandle handle) =>
|
|
handle is DelegateFontHandle cgfh ? this.fonts.GetValueOrDefault(cgfh) : default;
|
|
|
|
/// <inheritdoc/>
|
|
public Exception? GetBuildException(IFontHandle handle) =>
|
|
handle is DelegateFontHandle cgfh ? this.buildExceptions.GetValueOrDefault(cgfh) : default;
|
|
|
|
/// <inheritdoc/>
|
|
public void OnPreBuild(IFontAtlasBuildToolkitPreBuild toolkitPreBuild)
|
|
{
|
|
var fontsVector = toolkitPreBuild.Fonts;
|
|
foreach (var k in this.RelevantHandles)
|
|
{
|
|
var fontCountPrevious = fontsVector.Length;
|
|
|
|
try
|
|
{
|
|
toolkitPreBuild.Font = default;
|
|
k.CallOnBuildStepChange(toolkitPreBuild);
|
|
if (toolkitPreBuild.Font.IsNull())
|
|
{
|
|
if (fontCountPrevious == fontsVector.Length)
|
|
{
|
|
throw new InvalidOperationException(
|
|
$"{nameof(FontAtlasBuildStepDelegate)} must either set the " +
|
|
$"{nameof(IFontAtlasBuildToolkitPreBuild.Font)} property, or add at least one font.");
|
|
}
|
|
|
|
toolkitPreBuild.Font = fontsVector[^1];
|
|
}
|
|
else
|
|
{
|
|
var found = false;
|
|
unsafe
|
|
{
|
|
for (var i = fontCountPrevious; !found && i < fontsVector.Length; i++)
|
|
{
|
|
if (fontsVector[i].NativePtr == toolkitPreBuild.Font.NativePtr)
|
|
found = true;
|
|
}
|
|
}
|
|
|
|
if (!found)
|
|
{
|
|
throw new InvalidOperationException(
|
|
"The font does not exist in the atlas' font array. If you need an empty font, try" +
|
|
"adding Noto Sans from Dalamud Assets, but using new ushort[]{ ' ', ' ', 0 } as the" +
|
|
"glyph range.");
|
|
}
|
|
}
|
|
|
|
if (fontsVector.Length - fontCountPrevious != 1)
|
|
{
|
|
Log.Warning(
|
|
"[{name}:Substance] {n} fonts added from {delegate} PreBuild call; " +
|
|
"Using the most recently added font. " +
|
|
"Did you mean to use {sfd}.{sfdprop} or {ifcp}.{ifcpprop}?",
|
|
this.Manager.Name,
|
|
fontsVector.Length - fontCountPrevious,
|
|
nameof(FontAtlasBuildStepDelegate),
|
|
nameof(SafeFontConfig),
|
|
nameof(SafeFontConfig.MergeFont),
|
|
nameof(ImFontConfigPtr),
|
|
nameof(ImFontConfigPtr.MergeMode));
|
|
}
|
|
|
|
for (var i = fontCountPrevious; i < fontsVector.Length; i++)
|
|
{
|
|
if (fontsVector[i].ValidateUnsafe() is { } ex)
|
|
{
|
|
throw new InvalidOperationException(
|
|
"One of the newly added fonts seem to be pointing to an invalid memory address.",
|
|
ex);
|
|
}
|
|
}
|
|
|
|
// Check for duplicate entries; duplicates will result in free-after-free
|
|
for (var i = 0; i < fontCountPrevious; i++)
|
|
{
|
|
for (var j = fontCountPrevious; j < fontsVector.Length; j++)
|
|
{
|
|
unsafe
|
|
{
|
|
if (fontsVector[i].NativePtr == fontsVector[j].NativePtr)
|
|
throw new InvalidOperationException("An already added font has been added again.");
|
|
}
|
|
}
|
|
}
|
|
|
|
this.fonts[k] = toolkitPreBuild.Font;
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
this.fonts[k] = default;
|
|
this.buildExceptions[k] = e;
|
|
|
|
Log.Error(
|
|
e,
|
|
"[{name}:Substance] An error has occurred while during {delegate} PreBuild call.",
|
|
this.Manager.Name,
|
|
nameof(FontAtlasBuildStepDelegate));
|
|
|
|
// Sanitization, in a futile attempt to prevent crashes on invalid parameters
|
|
unsafe
|
|
{
|
|
var distinct =
|
|
fontsVector
|
|
.DistinctBy(x => (nint)x.NativePtr) // Remove duplicates
|
|
.Where(x => x.ValidateUnsafe() is null) // Remove invalid entries without freeing them
|
|
.ToArray();
|
|
|
|
// We're adding the contents back; do not destroy the contents
|
|
fontsVector.Clear(true);
|
|
fontsVector.AddRange(distinct.AsSpan());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public void OnPreBuildCleanup(IFontAtlasBuildToolkitPreBuild toolkitPreBuild)
|
|
{
|
|
// irrelevant
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public void OnPostBuild(IFontAtlasBuildToolkitPostBuild toolkitPostBuild)
|
|
{
|
|
foreach (var k in this.RelevantHandles)
|
|
{
|
|
if (!this.fonts[k].IsNotNullAndLoaded())
|
|
continue;
|
|
|
|
try
|
|
{
|
|
toolkitPostBuild.Font = this.fonts[k];
|
|
k.CallOnBuildStepChange.Invoke(toolkitPostBuild);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
this.fonts[k] = default;
|
|
this.buildExceptions[k] = e;
|
|
|
|
Log.Error(
|
|
e,
|
|
"[{name}] An error has occurred while during {delegate} PostBuild call.",
|
|
this.Manager.Name,
|
|
nameof(FontAtlasBuildStepDelegate));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|