Dalamud/Dalamud/Interface/ManagedFontAtlas/Internals/GamePrebakedFontHandle.cs
2024-04-20 23:25:33 -07:00

871 lines
35 KiB
C#

using System.Buffers;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Reactive.Disposables;
using Dalamud.Game.Text;
using Dalamud.Interface.GameFonts;
using Dalamud.Interface.Internal;
using Dalamud.Interface.Utility;
using Dalamud.Utility;
using ImGuiNET;
using Lumina.Data.Files;
using Vector4 = System.Numerics.Vector4;
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 : FontHandle
{
/// <summary>
/// The smallest value of <see cref="SeIconChar"/>.
/// </summary>
public static readonly char SeIconCharMin = (char)Enum.GetValues<SeIconChar>().Min();
/// <summary>
/// The largest value of <see cref="SeIconChar"/>.
/// </summary>
public static readonly char SeIconCharMax = (char)Enum.GetValues<SeIconChar>().Max();
/// <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);
if (style.SizePt <= 0)
throw new ArgumentException($"{nameof(style.SizePt)} must be a positive number.", nameof(style));
this.FontStyle = style;
}
/// <summary>
/// Provider for <see cref="IDalamudTextureWrap"/> for `common/font/fontNN.tex`.
/// </summary>
public interface IGameFontTextureProvider
{
/// <summary>
/// Creates the <see cref="FdtFileView"/> for the <see cref="GameFontFamilyAndSize"/>.<br />
/// <strong>Dispose after use.</strong>
/// </summary>
/// <param name="gffas">The font family and size.</param>
/// <param name="fdtFileView">The view.</param>
/// <returns>Dispose this after use..</returns>
public MemoryHandle CreateFdtFileView(GameFontFamilyAndSize gffas, out FdtFileView fdtFileView);
/// <summary>
/// Gets the number of font textures.
/// </summary>
/// <param name="texPathFormat">Format of .tex path.</param>
/// <returns>The number of textures.</returns>
public int GetFontTextureCount(string texPathFormat);
/// <summary>
/// Gets the <see cref="TexFile"/> for the given index of a font.
/// </summary>
/// <param name="texPathFormat">Format of .tex path.</param>
/// <param name="index">The index of .tex file.</param>
/// <returns>The <see cref="TexFile"/>.</returns>
public TexFile GetTexFile(string texPathFormat, int index);
/// <summary>
/// Gets a new reference of the font texture.
/// </summary>
/// <param name="texPathFormat">Format of .tex path.</param>
/// <param name="textureIndex">Texture index.</param>
/// <returns>The texture.</returns>
public IDalamudTextureWrap NewFontTextureRef(string texPathFormat, int textureIndex);
}
/// <summary>
/// Gets the font style.
/// </summary>
public GameFontStyle FontStyle { get; }
/// <inheritdoc/>
public override string ToString() => $"{nameof(GamePrebakedFontHandle)}({this.FontStyle})";
/// <summary>
/// Manager for <see cref="GamePrebakedFontHandle"/>s.
/// </summary>
internal sealed class HandleManager : IFontHandleManager
{
private readonly Dictionary<GameFontStyle, int> gameFontsRc = new();
private readonly HashSet<GamePrebakedFontHandle> 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>
/// <param name="gameFontTextureProvider">An instance of <see cref="IGameFontTextureProvider"/>.</param>
public HandleManager(string atlasName, IGameFontTextureProvider gameFontTextureProvider)
{
this.GameFontTextureProvider = gameFontTextureProvider;
this.Name = $"{atlasName}:{nameof(GamePrebakedFontHandle)}:Manager";
}
/// <inheritdoc/>
public event Action? RebuildRecommend;
/// <inheritdoc/>
public string Name { get; }
/// <inheritdoc/>
public IFontHandleSubstance? Substance { get; set; }
/// <summary>
/// Gets an instance of <see cref="IGameFontTextureProvider"/>.
/// </summary>
public IGameFontTextureProvider GameFontTextureProvider { get; }
/// <inheritdoc/>
public void Dispose()
{
// empty
}
/// <inheritdoc cref="IFontAtlas.NewGameFontHandle"/>
public IFontHandle NewFontHandle(GameFontStyle style)
{
var handle = new GamePrebakedFontHandle(this, style);
bool suggestRebuild;
lock (this.syncRoot)
{
this.handles.Add(handle);
this.gameFontsRc[style] = this.gameFontsRc.GetValueOrDefault(style, 0) + 1;
suggestRebuild = this.Substance?.GetFontPtr(handle).IsNotNullAndLoaded() is not true;
}
if (suggestRebuild)
this.RebuildRecommend?.Invoke();
return handle;
}
/// <inheritdoc/>
public void FreeFontHandle(IFontHandle handle)
{
if (handle is not GamePrebakedFontHandle ggfh)
return;
lock (this.syncRoot)
{
this.handles.Remove(ggfh);
if (!this.gameFontsRc.ContainsKey(ggfh.FontStyle))
return;
if ((this.gameFontsRc[ggfh.FontStyle] -= 1) == 0)
this.gameFontsRc.Remove(ggfh.FontStyle);
}
}
/// <inheritdoc/>
public IFontHandleSubstance NewSubstance(IRefCountable dataRoot)
{
lock (this.syncRoot)
return new HandleSubstance(this, dataRoot, this.handles.ToArray(), this.gameFontsRc.Keys);
}
}
/// <summary>
/// Substance from <see cref="HandleManager"/>.
/// </summary>
internal sealed class HandleSubstance : IFontHandleSubstance
{
private readonly HandleManager handleManager;
private readonly HashSet<GameFontStyle> gameFontStyles;
// Owned by this class, but ImFontPtr values still do not belong to this.
private readonly Dictionary<GameFontStyle, FontDrawPlan> fonts = new();
private readonly Dictionary<GameFontStyle, Exception?> buildExceptions = new();
private readonly List<(ImFontPtr Font, GameFontStyle Style, ushort[]? Ranges)> attachments = new();
private readonly HashSet<ImFontPtr> templatedFonts = 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>
/// <param name="gameFontStyles">The game font styles.</param>
public HandleSubstance(
HandleManager manager,
IRefCountable dataRoot,
GamePrebakedFontHandle[] relevantHandles,
IEnumerable<GameFontStyle> gameFontStyles)
{
// We do not call dataRoot.AddRef; this object is dependant on lifetime of dataRoot.
this.handleManager = manager;
this.DataRoot = dataRoot;
this.RelevantHandles = relevantHandles;
this.gameFontStyles = new(gameFontStyles);
}
/// <summary>
/// Gets the relevant handles.
/// </summary>
// Not owned by this class. Do not dispose.
public GamePrebakedFontHandle[] RelevantHandles { get; }
/// <inheritdoc/>
ICollection<FontHandle> IFontHandleSubstance.RelevantHandles => this.RelevantHandles;
/// <inheritdoc/>
public IRefCountable DataRoot { get; }
/// <inheritdoc/>
public IFontHandleManager Manager => this.handleManager;
/// <inheritdoc/>
public void Dispose()
{
// empty
}
/// <summary>
/// Attaches game symbols to the given font. If font is null, it will be created.
/// </summary>
/// <param name="toolkitPreBuild">The toolkitPostBuild.</param>
/// <param name="font">The font to attach to.</param>
/// <param name="style">The game font style.</param>
/// <param name="glyphRanges">The intended glyph ranges.</param>
/// <returns><paramref name="font"/> if it is not empty; otherwise a new font.</returns>
public ImFontPtr AttachGameGlyphs(
IFontAtlasBuildToolkitPreBuild toolkitPreBuild,
ImFontPtr font,
GameFontStyle style,
ushort[]? glyphRanges = null)
{
if (font.IsNull())
font = this.CreateTemplateFont(toolkitPreBuild, style.SizePx);
this.attachments.Add((font, style, glyphRanges));
return font;
}
/// <summary>
/// Creates or gets a relevant <see cref="ImFontPtr"/> for the given <see cref="GameFontStyle"/>.
/// </summary>
/// <param name="style">The game font style.</param>
/// <param name="toolkitPreBuild">The toolkitPostBuild.</param>
/// <returns>The font.</returns>
public ImFontPtr GetOrCreateFont(GameFontStyle style, IFontAtlasBuildToolkitPreBuild toolkitPreBuild)
{
try
{
if (!this.fonts.TryGetValue(style, out var plan))
{
plan = new(
style,
toolkitPreBuild.Scale,
this.handleManager.GameFontTextureProvider,
this.CreateTemplateFont(toolkitPreBuild, style.SizePx));
this.fonts[style] = plan;
}
plan.AttachFont(plan.FullRangeFont);
return plan.FullRangeFont;
}
catch (Exception e)
{
this.buildExceptions[style] = e;
throw;
}
}
/// <inheritdoc/>
public ImFontPtr GetFontPtr(IFontHandle handle) =>
handle is GamePrebakedFontHandle ggfh
? this.fonts.GetValueOrDefault(ggfh.FontStyle)?.FullRangeFont ?? default
: default;
/// <inheritdoc/>
public Exception? GetBuildException(IFontHandle handle) =>
handle is GamePrebakedFontHandle ggfh ? this.buildExceptions.GetValueOrDefault(ggfh.FontStyle) : default;
/// <inheritdoc/>
public void OnPreBuild(IFontAtlasBuildToolkitPreBuild toolkitPreBuild)
{
foreach (var style in this.gameFontStyles)
{
if (this.fonts.ContainsKey(style))
continue;
try
{
_ = this.GetOrCreateFont(style, toolkitPreBuild);
}
catch
{
// ignore; it should have been recorded from the call
}
}
}
/// <inheritdoc/>
public void OnPreBuildCleanup(IFontAtlasBuildToolkitPreBuild toolkitPreBuild)
{
foreach (var (font, style, ranges) in this.attachments)
{
if (!this.fonts.TryGetValue(style, out var plan))
{
switch (toolkitPreBuild.GetFontScaleMode(font))
{
case FontScaleMode.Default:
default:
plan = new(
style,
toolkitPreBuild.Scale,
this.handleManager.GameFontTextureProvider,
this.CreateTemplateFont(toolkitPreBuild, style.SizePx));
break;
case FontScaleMode.SkipHandling:
plan = new(
style,
1f,
this.handleManager.GameFontTextureProvider,
this.CreateTemplateFont(toolkitPreBuild, style.SizePx));
break;
case FontScaleMode.UndoGlobalScale:
plan = new(
style.Scale(1 / toolkitPreBuild.Scale),
toolkitPreBuild.Scale,
this.handleManager.GameFontTextureProvider,
this.CreateTemplateFont(toolkitPreBuild, style.SizePx));
break;
}
this.fonts[style] = plan;
}
plan.AttachFont(font, ranges);
}
foreach (var plan in this.fonts.Values)
{
plan.EnsureGlyphs(toolkitPreBuild.NewImAtlas);
}
}
/// <inheritdoc/>
public unsafe void OnPostBuild(IFontAtlasBuildToolkitPostBuild toolkitPostBuild)
{
var allTextureIndices = new Dictionary<string, int[]>();
var allTexFiles = new Dictionary<string, TexFile[]>();
using var rentReturn = Disposable.Create(
() =>
{
foreach (var x in allTextureIndices.Values)
ArrayPool<int>.Shared.Return(x);
foreach (var x in allTexFiles.Values)
ArrayPool<TexFile>.Shared.Return(x);
});
var pixels8Array = new byte*[toolkitPostBuild.NewImAtlas.Textures.Size];
var widths = new int[toolkitPostBuild.NewImAtlas.Textures.Size];
for (var i = 0; i < pixels8Array.Length; i++)
toolkitPostBuild.NewImAtlas.GetTexDataAsAlpha8(i, out pixels8Array[i], out widths[i], out _);
foreach (var (style, plan) in this.fonts)
{
try
{
foreach (var font in plan.Ranges.Keys)
this.PatchFontMetricsIfNecessary(style, font, toolkitPostBuild.Scale);
plan.SetFullRangeFontGlyphs(toolkitPostBuild, allTexFiles, allTextureIndices, pixels8Array, widths);
plan.CopyGlyphsToRanges(toolkitPostBuild);
plan.PostProcessFullRangeFont(toolkitPostBuild.Scale);
}
catch (Exception e)
{
this.buildExceptions[style] = e;
this.fonts[style] = default;
}
}
}
/// <summary>
/// Creates a new template font.
/// </summary>
/// <param name="toolkitPreBuild">The toolkitPostBuild.</param>
/// <param name="sizePx">The size of the font.</param>
/// <returns>The font.</returns>
private ImFontPtr CreateTemplateFont(IFontAtlasBuildToolkitPreBuild toolkitPreBuild, float sizePx)
{
var font = toolkitPreBuild.AddDalamudAssetFont(
DalamudAsset.NotoSansJpMedium,
new()
{
GlyphRanges = new ushort[] { ' ', ' ', '\0' },
SizePx = sizePx,
});
this.templatedFonts.Add(font);
return font;
}
private unsafe void PatchFontMetricsIfNecessary(GameFontStyle style, ImFontPtr font, float atlasScale)
{
if (!this.templatedFonts.Contains(font))
return;
var fas = style.Scale(atlasScale).FamilyAndSize;
using var handle = this.handleManager.GameFontTextureProvider.CreateFdtFileView(fas, out var fdt);
ref var fdtFontHeader = ref fdt.FontHeader;
var fontPtr = font.NativePtr;
var scale = style.SizePt / fdtFontHeader.Size;
fontPtr->Ascent = fdtFontHeader.Ascent * scale;
fontPtr->Descent = fdtFontHeader.Descent * scale;
fontPtr->EllipsisChar = '…';
}
}
[SuppressMessage(
"StyleCop.CSharp.MaintainabilityRules",
"SA1401:Fields should be private",
Justification = "Internal")]
private sealed class FontDrawPlan : IDisposable
{
public readonly GameFontStyle Style;
public readonly GameFontStyle BaseStyle;
public readonly GameFontFamilyAndSizeAttribute BaseAttr;
public readonly int TexCount;
public readonly Dictionary<ImFontPtr, BitArray> Ranges = new();
public readonly List<(int RectId, int FdtGlyphIndex)> Rects = new();
public readonly ushort[] RectLookup = new ushort[0x10000];
public readonly FdtFileView Fdt;
public readonly ImFontPtr FullRangeFont;
private readonly IDisposable fdtHandle;
private readonly IGameFontTextureProvider gftp;
public FontDrawPlan(
GameFontStyle style,
float scale,
IGameFontTextureProvider gameFontTextureProvider,
ImFontPtr fullRangeFont)
{
this.Style = style;
this.BaseStyle = style.Scale(scale);
this.BaseAttr = this.BaseStyle.FamilyAndSize.GetAttribute<GameFontFamilyAndSizeAttribute>()!;
this.gftp = gameFontTextureProvider;
this.TexCount = this.gftp.GetFontTextureCount(this.BaseAttr.TexPathFormat);
this.fdtHandle = this.gftp.CreateFdtFileView(this.BaseStyle.FamilyAndSize, out this.Fdt);
this.RectLookup.AsSpan().Fill(ushort.MaxValue);
this.FullRangeFont = fullRangeFont;
this.Ranges[fullRangeFont] = new(0x10000);
}
public void Dispose()
{
this.fdtHandle.Dispose();
}
public void AttachFont(ImFontPtr font, ushort[]? glyphRanges = null)
{
if (!this.Ranges.TryGetValue(font, out var rangeBitArray))
rangeBitArray = this.Ranges[font] = new(0x10000);
if (glyphRanges is null)
{
foreach (ref var g in this.Fdt.Glyphs)
{
var c = g.CharInt;
if (c is >= 0x20 and <= 0xFFFE)
rangeBitArray[c] = true;
}
return;
}
for (var i = 0; i < glyphRanges.Length - 1; i += 2)
{
if (glyphRanges[i] == 0)
break;
var from = (int)glyphRanges[i];
var to = (int)glyphRanges[i + 1];
for (var j = from; j <= to; j++)
rangeBitArray[j] = true;
}
}
public unsafe void EnsureGlyphs(ImFontAtlasPtr atlas)
{
var glyphs = this.Fdt.Glyphs;
var ranges = this.Ranges[this.FullRangeFont];
foreach (var (font, extraRange) in this.Ranges)
{
if (font.NativePtr != this.FullRangeFont.NativePtr)
ranges.Or(extraRange);
}
if (this.Style is not { Weight: 0, SkewStrength: 0 })
{
for (var fdtGlyphIndex = 0; fdtGlyphIndex < glyphs.Length; fdtGlyphIndex++)
{
ref var glyph = ref glyphs[fdtGlyphIndex];
var cint = glyph.CharInt;
if (cint > char.MaxValue)
continue;
if (!ranges[cint] || this.RectLookup[cint] != ushort.MaxValue)
continue;
var widthAdjustment = this.BaseStyle.CalculateBaseWidthAdjustment(this.Fdt.FontHeader, glyph);
this.RectLookup[cint] = (ushort)this.Rects.Count;
this.Rects.Add(
(
atlas.AddCustomRectFontGlyph(
this.FullRangeFont,
(char)cint,
glyph.BoundingWidth + widthAdjustment,
glyph.BoundingHeight,
glyph.AdvanceWidth,
new(this.BaseAttr.HorizontalOffset, glyph.CurrentOffsetY)),
fdtGlyphIndex));
}
}
else
{
for (var fdtGlyphIndex = 0; fdtGlyphIndex < glyphs.Length; fdtGlyphIndex++)
{
ref var glyph = ref glyphs[fdtGlyphIndex];
var cint = glyph.CharInt;
if (cint > char.MaxValue)
continue;
if (!ranges[cint] || this.RectLookup[cint] != ushort.MaxValue)
continue;
this.RectLookup[cint] = (ushort)this.Rects.Count;
this.Rects.Add((-1, fdtGlyphIndex));
}
}
}
public unsafe void PostProcessFullRangeFont(float atlasScale)
{
var round = 1 / atlasScale;
var pfrf = this.FullRangeFont.NativePtr;
ref var frf = ref *pfrf;
frf.FontSize = MathF.Round(frf.FontSize / round) * round;
frf.Ascent = MathF.Round(frf.Ascent / round) * round;
frf.Descent = MathF.Round(frf.Descent / round) * round;
var scale = this.Style.SizePt / this.Fdt.FontHeader.Size;
foreach (ref var g in this.FullRangeFont.GlyphsWrapped().DataSpan)
{
var w = (g.X1 - g.X0) * scale;
var h = (g.Y1 - g.Y0) * scale;
g.X0 = MathF.Round((g.X0 * scale) / round) * round;
g.Y0 = MathF.Round((g.Y0 * scale) / round) * round;
g.X1 = g.X0 + w;
g.Y1 = g.Y0 + h;
g.AdvanceX = MathF.Round((g.AdvanceX * scale) / round) * round;
}
var fullRange = this.Ranges[this.FullRangeFont];
foreach (ref var k in this.Fdt.PairAdjustments)
{
var (leftInt, rightInt) = (k.LeftInt, k.RightInt);
if (leftInt > char.MaxValue || rightInt > char.MaxValue)
continue;
if (!fullRange[leftInt] || !fullRange[rightInt])
continue;
ImGuiNative.ImFont_AddKerningPair(
pfrf,
(ushort)leftInt,
(ushort)rightInt,
MathF.Round((k.RightOffset * scale) / round) * round);
}
pfrf->FallbackGlyph = null;
ImGuiNative.ImFont_BuildLookupTable(pfrf);
foreach (var fallbackCharCandidate in FontAtlasFactory.FallbackCodepoints)
{
var glyph = ImGuiNative.ImFont_FindGlyphNoFallback(pfrf, fallbackCharCandidate);
if ((nint)glyph == IntPtr.Zero)
continue;
frf.FallbackChar = fallbackCharCandidate;
frf.FallbackGlyph = glyph;
frf.FallbackHotData =
(ImFontGlyphHotData*)frf.IndexedHotData.Address<ImGuiHelpers.ImFontGlyphHotDataReal>(
fallbackCharCandidate);
break;
}
}
public unsafe void CopyGlyphsToRanges(IFontAtlasBuildToolkitPostBuild toolkitPostBuild)
{
var scale = this.Style.SizePt / this.Fdt.FontHeader.Size;
foreach (var (font, rangeBits) in this.Ranges)
{
if (font.NativePtr == this.FullRangeFont.NativePtr)
continue;
var fontScaleMode = toolkitPostBuild.GetFontScaleMode(font);
var round = fontScaleMode == FontScaleMode.SkipHandling ? 1 : 1 / toolkitPostBuild.Scale;
var lookup = font.IndexLookupWrapped();
var glyphs = font.GlyphsWrapped();
foreach (ref var sourceGlyph in this.FullRangeFont.GlyphsWrapped().DataSpan)
{
if (!rangeBits[sourceGlyph.Codepoint])
continue;
var glyphIndex = ushort.MaxValue;
if (sourceGlyph.Codepoint < lookup.Length)
glyphIndex = lookup[sourceGlyph.Codepoint];
if (glyphIndex == ushort.MaxValue)
{
glyphIndex = (ushort)glyphs.Length;
glyphs.Add(default);
}
ref var g = ref glyphs[glyphIndex];
g = sourceGlyph;
if (fontScaleMode == FontScaleMode.SkipHandling)
{
g.XY *= scale;
g.AdvanceX *= scale;
}
else
{
var w = (g.X1 - g.X0) * scale;
var h = (g.Y1 - g.Y0) * scale;
g.X0 = MathF.Round((g.X0 * scale) / round) * round;
g.Y0 = MathF.Round((g.Y0 * scale) / round) * round;
g.X1 = g.X0 + w;
g.Y1 = g.Y0 + h;
g.AdvanceX = MathF.Round((g.AdvanceX * scale) / round) * round;
}
}
foreach (ref var k in this.Fdt.PairAdjustments)
{
var (leftInt, rightInt) = (k.LeftInt, k.RightInt);
if (leftInt > char.MaxValue || rightInt > char.MaxValue)
continue;
if (!rangeBits[leftInt] || !rangeBits[rightInt])
continue;
if (fontScaleMode == FontScaleMode.SkipHandling)
{
font.AddKerningPair((ushort)leftInt, (ushort)rightInt, k.RightOffset * scale);
}
else
{
font.AddKerningPair(
(ushort)leftInt,
(ushort)rightInt,
MathF.Round((k.RightOffset * scale) / round) * round);
}
}
font.NativePtr->FallbackGlyph = null;
font.BuildLookupTable();
foreach (var fallbackCharCandidate in FontAtlasFactory.FallbackCodepoints)
{
var glyph = font.FindGlyphNoFallback(fallbackCharCandidate).NativePtr;
if ((nint)glyph == IntPtr.Zero)
continue;
ref var frf = ref *font.NativePtr;
frf.FallbackChar = fallbackCharCandidate;
frf.FallbackGlyph = glyph;
frf.FallbackHotData =
(ImFontGlyphHotData*)frf.IndexedHotData.Address<ImGuiHelpers.ImFontGlyphHotDataReal>(
fallbackCharCandidate);
break;
}
}
}
public unsafe void SetFullRangeFontGlyphs(
IFontAtlasBuildToolkitPostBuild toolkitPostBuild,
Dictionary<string, TexFile[]> allTexFiles,
Dictionary<string, int[]> allTextureIndices,
byte*[] pixels8Array,
int[] widths)
{
var glyphs = this.FullRangeFont.GlyphsWrapped();
var lookups = this.FullRangeFont.IndexLookupWrapped();
ref var fdtFontHeader = ref this.Fdt.FontHeader;
var fdtGlyphs = this.Fdt.Glyphs;
var fdtTexSize = new Vector4(
this.Fdt.FontHeader.TextureWidth,
this.Fdt.FontHeader.TextureHeight,
this.Fdt.FontHeader.TextureWidth,
this.Fdt.FontHeader.TextureHeight);
if (!allTexFiles.TryGetValue(this.BaseAttr.TexPathFormat, out var texFiles))
{
allTexFiles.Add(
this.BaseAttr.TexPathFormat,
texFiles = ArrayPool<TexFile>.Shared.Rent(this.TexCount));
}
if (!allTextureIndices.TryGetValue(this.BaseAttr.TexPathFormat, out var textureIndices))
{
allTextureIndices.Add(
this.BaseAttr.TexPathFormat,
textureIndices = ArrayPool<int>.Shared.Rent(this.TexCount));
textureIndices.AsSpan(0, this.TexCount).Fill(-1);
}
var pixelWidth = Math.Max(1, (int)MathF.Ceiling(this.BaseStyle.Weight + 1));
var pixelStrength = stackalloc byte[pixelWidth];
for (var i = 0; i < pixelWidth; i++)
pixelStrength[i] = (byte)(255 * Math.Min(1f, (this.BaseStyle.Weight + 1) - i));
var minGlyphY = 0;
var maxGlyphY = 0;
foreach (ref var g in fdtGlyphs)
{
minGlyphY = Math.Min(g.CurrentOffsetY, minGlyphY);
maxGlyphY = Math.Max(g.BoundingHeight + g.CurrentOffsetY, maxGlyphY);
}
var horzShift = stackalloc int[maxGlyphY - minGlyphY];
var horzBlend = stackalloc byte[maxGlyphY - minGlyphY];
horzShift -= minGlyphY;
horzBlend -= minGlyphY;
if (this.BaseStyle.BaseSkewStrength != 0)
{
for (var i = minGlyphY; i < maxGlyphY; i++)
{
float blend = this.BaseStyle.BaseSkewStrength switch
{
> 0 => fdtFontHeader.LineHeight - i,
< 0 => -i,
_ => throw new InvalidOperationException(),
};
blend *= this.BaseStyle.BaseSkewStrength / fdtFontHeader.LineHeight;
horzShift[i] = (int)MathF.Floor(blend);
horzBlend[i] = (byte)(255 * (blend - horzShift[i]));
}
}
foreach (var (rectId, fdtGlyphIndex) in this.Rects)
{
ref var fdtGlyph = ref fdtGlyphs[fdtGlyphIndex];
if (rectId == -1)
{
ref var textureIndex = ref textureIndices[fdtGlyph.TextureIndex];
if (textureIndex == -1)
{
textureIndex = toolkitPostBuild.StoreTexture(
this.gftp.NewFontTextureRef(this.BaseAttr.TexPathFormat, fdtGlyph.TextureIndex),
true);
}
var glyph = new ImGuiHelpers.ImFontGlyphReal
{
AdvanceX = fdtGlyph.AdvanceWidth,
Codepoint = fdtGlyph.Char,
Colored = false,
TextureIndex = textureIndex,
Visible = true,
X0 = this.BaseAttr.HorizontalOffset,
Y0 = fdtGlyph.CurrentOffsetY,
U0 = fdtGlyph.TextureOffsetX,
V0 = fdtGlyph.TextureOffsetY,
U1 = fdtGlyph.BoundingWidth,
V1 = fdtGlyph.BoundingHeight,
};
glyph.XY1 = glyph.XY0 + glyph.UV1;
glyph.UV1 += glyph.UV0;
glyph.UV /= fdtTexSize;
glyphs.Add(glyph);
}
else
{
ref var rc = ref *(ImGuiHelpers.ImFontAtlasCustomRectReal*)toolkitPostBuild.NewImAtlas
.GetCustomRectByIndex(rectId)
.NativePtr;
var widthAdjustment = this.BaseStyle.CalculateBaseWidthAdjustment(fdtFontHeader, fdtGlyph);
// Glyph is scaled at this point; undo that.
ref var glyph = ref glyphs[lookups[rc.GlyphId]];
glyph.X0 = this.BaseAttr.HorizontalOffset;
glyph.Y0 = fdtGlyph.CurrentOffsetY;
glyph.X1 = glyph.X0 + fdtGlyph.BoundingWidth + widthAdjustment;
glyph.Y1 = glyph.Y0 + fdtGlyph.BoundingHeight;
glyph.AdvanceX = fdtGlyph.AdvanceWidth;
var pixels8 = pixels8Array[rc.TextureIndex];
var width = widths[rc.TextureIndex];
texFiles[fdtGlyph.TextureFileIndex] ??=
this.gftp.GetTexFile(this.BaseAttr.TexPathFormat, fdtGlyph.TextureFileIndex);
var sourceBuffer = texFiles[fdtGlyph.TextureFileIndex].ImageData;
var sourceBufferDelta = fdtGlyph.TextureChannelByteIndex;
for (var y = 0; y < fdtGlyph.BoundingHeight; y++)
{
var sourcePixelIndex =
((fdtGlyph.TextureOffsetY + y) * fdtFontHeader.TextureWidth) + fdtGlyph.TextureOffsetX;
sourcePixelIndex *= 4;
sourcePixelIndex += sourceBufferDelta;
var blend1 = horzBlend[fdtGlyph.CurrentOffsetY + y];
var targetOffset = ((rc.Y + y) * width) + rc.X;
for (var x = 0; x < rc.Width; x++)
pixels8[targetOffset + x] = 0;
targetOffset += horzShift[fdtGlyph.CurrentOffsetY + y];
if (blend1 == 0)
{
for (var x = 0; x < fdtGlyph.BoundingWidth; x++, sourcePixelIndex += 4, targetOffset++)
{
var n = sourceBuffer[sourcePixelIndex + 4];
for (var boldOffset = 0; boldOffset < pixelWidth; boldOffset++)
{
ref var p = ref pixels8[targetOffset + boldOffset];
p = Math.Max(p, (byte)((pixelStrength[boldOffset] * n) / 255));
}
}
}
else
{
var blend2 = 255 - blend1;
for (var x = 0; x < fdtGlyph.BoundingWidth; x++, sourcePixelIndex += 4, targetOffset++)
{
var a1 = sourceBuffer[sourcePixelIndex];
var a2 = x == fdtGlyph.BoundingWidth - 1 ? 0 : sourceBuffer[sourcePixelIndex + 4];
var n = (a1 * blend1) + (a2 * blend2);
for (var boldOffset = 0; boldOffset < pixelWidth; boldOffset++)
{
ref var p = ref pixels8[targetOffset + boldOffset];
p = Math.Max(p, (byte)((pixelStrength[boldOffset] * n) / 255 / 255));
}
}
}
}
}
}
}
}
}