Dalamud/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs
2025-12-04 23:33:48 +01:00

861 lines
33 KiB
C#

using System.Buffers;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using Dalamud.Bindings.ImGui;
using Dalamud.Configuration.Internal;
using Dalamud.Interface.FontIdentifier;
using Dalamud.Interface.GameFonts;
using Dalamud.Interface.Internal;
using Dalamud.Interface.Textures;
using Dalamud.Interface.Textures.TextureWraps;
using Dalamud.Interface.Utility;
using Dalamud.Storage.Assets;
using Dalamud.Utility;
using TerraFX.Interop.DirectX;
namespace Dalamud.Interface.ManagedFontAtlas.Internals;
/// <summary>
/// Standalone font atlas.
/// </summary>
internal sealed partial class FontAtlasFactory
{
private static readonly Dictionary<ulong, List<(char Left, char Right, float Distance)>> PairAdjustmentsCache =
new();
/// <summary>
/// Implementations for <see cref="IFontAtlasBuildToolkitPreBuild"/> and
/// <see cref="IFontAtlasBuildToolkitPostBuild"/>.
/// </summary>
private class BuildToolkit : IFontAtlasBuildToolkitPreBuild, IFontAtlasBuildToolkitPostBuild, IDisposable
{
private static readonly ushort FontAwesomeIconMin =
(ushort)Enum.GetValues<FontAwesomeIcon>().Where(x => x > 0).Min();
private static readonly ushort FontAwesomeIconMax =
(ushort)Enum.GetValues<FontAwesomeIcon>().Where(x => x > 0).Max();
private readonly DisposeSafety.ScopedFinalizer disposeAfterBuild = new();
private readonly GamePrebakedFontHandle.HandleSubstance gameFontHandleSubstance;
private readonly FontAtlasFactory factory;
private readonly FontAtlasBuiltData data;
private readonly List<Action> registeredPostBuildActions = new();
/// <summary>
/// Initializes a new instance of the <see cref="BuildToolkit"/> class.
/// </summary>
/// <param name="factory">An instance of <see cref="FontAtlasFactory"/>.</param>
/// <param name="data">New atlas.</param>
/// <param name="gameFontHandleSubstance">An instance of <see cref="GamePrebakedFontHandle.HandleSubstance"/>.</param>
/// <param name="isAsync">Specify whether the current build operation is an asynchronous one.</param>
public BuildToolkit(
FontAtlasFactory factory,
FontAtlasBuiltData data,
GamePrebakedFontHandle.HandleSubstance gameFontHandleSubstance,
bool isAsync)
{
this.data = data;
this.gameFontHandleSubstance = gameFontHandleSubstance;
this.IsAsyncBuildOperation = isAsync;
this.factory = factory;
}
/// <inheritdoc cref="IFontAtlasBuildToolkit.Font"/>
public ImFontPtr Font { get; set; }
/// <inheritdoc cref="IFontAtlasBuildToolkit.Font"/>
public float Scale => this.data.Scale;
/// <inheritdoc/>
public bool IsAsyncBuildOperation { get; }
/// <inheritdoc/>
public FontAtlasBuildStep BuildStep { get; set; }
/// <inheritdoc/>
public ImFontAtlasPtr NewImAtlas => this.data.Atlas;
/// <inheritdoc/>
public ImVectorWrapper<ImFontPtr> Fonts => this.data.Fonts;
/// <summary>
/// Gets the font scale modes.
/// </summary>
private Dictionary<ImFontPtr, FontScaleMode> FontScaleModes { get; } = new();
/// <inheritdoc/>
public void Dispose() => this.disposeAfterBuild.Dispose();
/// <inheritdoc/>
public T2 DisposeAfterBuild<T2>(T2 disposable) where T2 : IDisposable =>
this.disposeAfterBuild.Add(disposable);
/// <inheritdoc/>
public GCHandle DisposeAfterBuild(GCHandle gcHandle) => this.disposeAfterBuild.Add(gcHandle);
/// <inheritdoc/>
public void DisposeAfterBuild(Action action) => this.disposeAfterBuild.Add(action);
/// <inheritdoc/>
public T DisposeWithAtlas<T>(T disposable) where T : IDisposable => this.data.Garbage.Add(disposable);
/// <inheritdoc/>
public GCHandle DisposeWithAtlas(GCHandle gcHandle) => this.data.Garbage.Add(gcHandle);
/// <inheritdoc/>
public void DisposeWithAtlas(Action action) => this.data.Garbage.Add(action);
/// <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 SetFontScaleMode(ImFontPtr fontPtr, FontScaleMode scaleMode)
{
this.FontScaleModes[fontPtr] = scaleMode;
return fontPtr;
}
/// <inheritdoc cref="IFontAtlasBuildToolkitPreBuild.GetFontScaleMode"/>
public FontScaleMode GetFontScaleMode(ImFontPtr fontPtr) =>
this.FontScaleModes.GetValueOrDefault(fontPtr, FontScaleMode.Default);
/// <inheritdoc/>
public int StoreTexture(IDalamudTextureWrap textureWrap, bool disposeOnError) =>
this.data.AddNewTexture(textureWrap, disposeOnError);
/// <inheritdoc/>
public void RegisterPostBuild(Action action) => this.registeredPostBuildActions.Add(action);
/// <inheritdoc/>
public unsafe ImFontPtr AddFontFromImGuiHeapAllocatedMemory(
void* dataPointer,
int dataSize,
in SafeFontConfig fontConfig,
bool freeOnException,
string debugTag)
{
Log.Verbose(
"[{name}] 0x{atlas:X}: {funcname}(0x{dataPointer:X}, 0x{dataSize:X}, ...) from {tag}",
this.data.Owner?.Name ?? "(error)",
(nint)this.NewImAtlas.Handle,
nameof(this.AddFontFromImGuiHeapAllocatedMemory),
(nint)dataPointer,
dataSize,
debugTag);
var font = default(ImFontPtr);
try
{
fontConfig.ThrowOnInvalidValues();
var raw = fontConfig.Raw with
{
FontData = dataPointer,
FontDataOwnedByAtlas = true,
FontDataSize = dataSize,
};
if (fontConfig.GlyphRanges is not { Length: > 0 } ranges)
ranges = new ushort[] { 1, 0xFFFE, 0 };
raw.GlyphRanges = (ushort*)this.DisposeAfterBuild(
GCHandle.Alloc(ranges, GCHandleType.Pinned)).AddrOfPinnedObject();
TrueTypeUtils.CheckImGuiCompatibleOrThrow(raw);
font = this.NewImAtlas.AddFont(raw);
var dataHash = default(HashCode);
dataHash.AddBytes(new(dataPointer, dataSize));
var hashIdent = (uint)dataHash.ToHashCode() | ((ulong)dataSize << 32);
List<(char Left, char Right, float Distance)> pairAdjustments;
lock (PairAdjustmentsCache)
{
if (!PairAdjustmentsCache.TryGetValue(hashIdent, out pairAdjustments))
{
PairAdjustmentsCache.Add(hashIdent, pairAdjustments = new());
try
{
pairAdjustments.AddRange(TrueTypeUtils.ExtractHorizontalPairAdjustments(raw).ToArray());
}
catch
{
// don't care
}
}
}
foreach (var pair in pairAdjustments)
{
if (!ImGuiHelpers.IsCodepointInSuppliedGlyphRangesUnsafe(pair.Left, raw.GlyphRanges))
continue;
if (!ImGuiHelpers.IsCodepointInSuppliedGlyphRangesUnsafe(pair.Right, raw.GlyphRanges))
continue;
font.AddKerningPair(pair.Left, pair.Right, pair.Distance * raw.SizePixels);
}
return font;
}
catch
{
if (!font.IsNull)
{
// Note that for both RemoveAt calls, corresponding destructors will be called.
var configIndex = this.data.ConfigData.FindIndex(x => x.DstFont == font.Handle);
if (configIndex >= 0)
this.data.ConfigData.RemoveAt(configIndex);
var index = this.Fonts.IndexOf(font);
if (index >= 0)
this.Fonts.RemoveAt(index);
}
// ImFontConfig has no destructor, and does not free the data.
if (freeOnException)
ImGui.MemFree(dataPointer);
throw;
}
}
/// <inheritdoc/>
public ImFontPtr AddFontFromFile(string path, in SafeFontConfig fontConfig)
{
return this.AddFontFromStream(
File.OpenRead(path),
fontConfig,
false,
$"{nameof(this.AddFontFromFile)}({path})");
}
/// <inheritdoc/>
public unsafe ImFontPtr AddFontFromStream(
Stream stream,
in SafeFontConfig fontConfig,
bool leaveOpen,
string debugTag)
{
using var streamCloser = leaveOpen ? null : stream;
if (!stream.CanSeek)
{
// There is no need to dispose a MemoryStream.
var ms = new MemoryStream();
stream.CopyTo(ms);
stream = ms;
}
var length = checked((uint)stream.Length);
var memory = ImGuiHelpers.AllocateMemory(length);
try
{
stream.ReadExactly(new(memory, checked((int)length)));
return this.AddFontFromImGuiHeapAllocatedMemory(
memory,
checked((int)length),
fontConfig,
false,
$"{nameof(this.AddFontFromStream)}({debugTag})");
}
catch
{
ImGui.MemFree(memory);
throw;
}
}
/// <inheritdoc/>
public unsafe ImFontPtr AddFontFromMemory(
ReadOnlySpan<byte> span,
in SafeFontConfig fontConfig,
string debugTag)
{
var length = span.Length;
var memory = ImGuiHelpers.AllocateMemory(checked((uint)length));
try
{
span.CopyTo(new(memory, length));
return this.AddFontFromImGuiHeapAllocatedMemory(
memory,
length,
fontConfig,
false,
$"{nameof(this.AddFontFromMemory)}({debugTag})");
}
catch
{
ImGui.MemFree(memory);
throw;
}
}
/// <inheritdoc/>
public ImFontPtr AddDalamudDefaultFont(float sizePx, ushort[]? glyphRanges)
{
ImFontPtr font = default;
glyphRanges ??= this.factory.DefaultGlyphRanges;
var dfid = this.factory.DefaultFontSpec;
if (sizePx < 0f)
sizePx *= -dfid.SizePx;
if (dfid is SingleFontSpec sfs)
{
if (sfs.FontId is DalamudDefaultFontAndFamilyId)
{
// invalid; calling sfs.AddToBuildToolkit calls this function, causing infinite recursion
}
else
{
sfs = sfs with { SizePx = sizePx };
font = sfs.AddToBuildToolkit(this);
if (sfs.FontId is not GameFontAndFamilyId { GameFontFamily: GameFontFamily.Axis })
this.AddGameSymbol(new() { SizePx = sizePx, MergeFont = font });
}
}
if (font.IsNull)
{
// fall back to AXIS fonts
font = this.AddGameGlyphs(new(GameFontFamily.Axis, sizePx), glyphRanges, default);
}
this.AttachExtraGlyphsForDalamudLanguage(new() { SizePx = sizePx, MergeFont = font });
if (this.Font.IsNull)
this.Font = font;
return font;
}
/// <inheritdoc/>
public ImFontPtr AddDalamudAssetFont(DalamudAsset asset, in SafeFontConfig fontConfig)
{
if (asset.GetPurpose() != DalamudAssetPurpose.Font)
throw new ArgumentOutOfRangeException(nameof(asset), asset, "Must have the purpose of Font.");
switch (asset)
{
case DalamudAsset.LodestoneGameSymbol when this.factory.HasGameSymbolsFontFile:
return this.factory.AddFont(
this,
asset,
fontConfig with
{
FontNo = 0,
SizePx = (fontConfig.SizePx * 3) / 2,
});
case DalamudAsset.LodestoneGameSymbol when !this.factory.HasGameSymbolsFontFile:
return this.AddGameGlyphs(
new(GameFontFamily.Axis, fontConfig.SizePx),
fontConfig.GlyphRanges,
fontConfig.MergeFont);
default:
return this.factory.AddFont(
this,
asset,
fontConfig with
{
FontNo = 0,
});
}
}
/// <inheritdoc/>
public ImFontPtr AddFontAwesomeIconFont(in SafeFontConfig fontConfig) => this.AddDalamudAssetFont(
DalamudAsset.FontAwesomeFreeSolid,
fontConfig with
{
GlyphRanges = new ushort[] { FontAwesomeIconMin, FontAwesomeIconMax, 0 },
});
/// <inheritdoc/>
public ImFontPtr AddGameSymbol(in SafeFontConfig fontConfig) =>
this.AddDalamudAssetFont(
DalamudAsset.LodestoneGameSymbol,
fontConfig with
{
GlyphRanges = new ushort[]
{
GamePrebakedFontHandle.SeIconCharMin,
GamePrebakedFontHandle.SeIconCharMax,
0,
},
});
/// <inheritdoc/>
public ImFontPtr AddGameGlyphs(GameFontStyle gameFontStyle, ushort[]? glyphRanges, ImFontPtr mergeFont) =>
this.gameFontHandleSubstance.AttachGameGlyphs(this, mergeFont, gameFontStyle, glyphRanges);
/// <inheritdoc/>
public void AttachWindowsDefaultFont(
CultureInfo cultureInfo,
in SafeFontConfig fontConfig,
int weight = (int)DWRITE_FONT_WEIGHT.DWRITE_FONT_WEIGHT_NORMAL,
int stretch = (int)DWRITE_FONT_STRETCH.DWRITE_FONT_STRETCH_NORMAL,
int style = (int)DWRITE_FONT_STYLE.DWRITE_FONT_STYLE_NORMAL)
{
var targetFont = fontConfig.MergeFont;
if (targetFont.IsNull)
targetFont = this.Font;
if (targetFont.IsNull)
return;
// https://learn.microsoft.com/en-us/windows/apps/design/globalizing/loc-international-fonts
var splitTag = cultureInfo.IetfLanguageTag.Split("-");
foreach (var test in new[]
{
cultureInfo.IetfLanguageTag,
$"{splitTag[0]}-{splitTag[^1]}",
})
{
var familyName = test switch
{
"af-ZA" => "Segoe UI",
"am-ET" => "Ebrima",
"ar-SA" => "Segoe UI",
"as-IN" => "Nirmala UI",
"az-Latn-AZ" => "Segoe UI",
"be-BY" => "Segoe UI",
"bg-BG" => "Segoe UI",
"bn-BD" => "Nirmala UI",
"bn-IN" => "Nirmala UI",
"bs-Latn-BA" => "Segoe UI",
"ca-ES" => "Segoe UI",
"ca-ES-valencia" => "Segoe UI",
"chr-CHER-US" => "Gadugi",
"cs-CZ" => "Segoe UI",
"cy-GB" => "Segoe UI",
"da-DK" => "Segoe UI",
"de-DE" => "Segoe UI",
"el-GR" => "Segoe UI",
"en-GB" => "Segoe UI",
"es-ES" => "Segoe UI",
"et-EE" => "Segoe UI",
"eu-ES" => "Segoe UI",
"fa-IR" => "Segoe UI",
"fi-FI" => "Segoe UI",
"fil-PH" => "Segoe UI",
"fr-FR" => "Segoe UI",
"ga-IE" => "Segoe UI",
"gd-GB" => "Segoe UI",
"gl-ES" => "Segoe UI",
"gu-IN" => "Nirmala UI",
"ha-Latn-NG" => "Segoe UI",
"he-IL" => "Segoe UI",
"hi-IN" => "Nirmala UI",
"hr-HR" => "Segoe UI",
"hu-HU" => "Segoe UI",
"hy-AM" => "Segoe UI",
"id-ID" => "Segoe UI",
"ig-NG" => "Segoe UI",
"is-IS" => "Segoe UI",
"it-IT" => "Segoe UI",
"ja-JP" => "Yu Gothic UI",
"ka-GE" => "Segoe UI",
"kk-KZ" => "Segoe UI",
"km-KH" => "Leelawadee UI",
"kn-IN" => "Nirmala UI",
"ko-KR" => "Malgun Gothic",
"kok-IN" => "Nirmala UI",
"ku-ARAB-IQ" => "Segoe UI",
"ky-KG" => "Segoe UI",
"lb-LU" => "Segoe UI",
"lt-LT" => "Segoe UI",
"lv-LV" => "Segoe UI",
"mi-NZ" => "Segoe UI",
"mk-MK" => "Segoe UI",
"ml-IN" => "Nirmala UI",
"mn-MN" => "Segoe UI",
"mr-IN" => "Nirmala UI",
"ms-MY" => "Segoe UI",
"mt-MT" => "Segoe UI",
"nb-NO" => "Segoe UI",
"ne-NP" => "Nirmala UI",
"nl-NL" => "Segoe UI",
"nn-NO" => "Segoe UI",
"nso-ZA" => "Segoe UI",
"or-IN" => "Nirmala UI",
"pa-Arab-PK" => "Segoe UI",
"pa-IN" => "Nirmala UI",
"pl-PL" => "Segoe UI",
"prs-AF" => "Segoe UI",
"pt-BR" => "Segoe UI",
"pt-PT" => "Segoe UI",
"qut-GT" => "Segoe UI",
"quz-PE" => "Segoe UI",
"ro-RO" => "Segoe UI",
"ru-RU" => "Segoe UI",
"rw-RW" => "Segoe UI",
"sd-Arab-PK" => "Segoe UI",
"si-LK" => "Nirmala UI",
"sk-SK" => "Segoe UI",
"sl-SI" => "Segoe UI",
"sq-AL" => "Segoe UI",
"sr-Cyrl-BA" => "Segoe UI",
"sr-Cyrl-CS" => "Segoe UI",
"sr-Latn-CS" => "Segoe UI",
"sv-SE" => "Segoe UI",
"sw-KE" => "Segoe UI",
"ta-IN" => "Nirmala UI",
"te-IN" => "Nirmala UI",
"tg-Cyrl-TJ" => "Segoe UI",
"th-TH" => "Leelawadee UI",
"ti-ET" => "Ebrima",
"tk-TM" => "Segoe UI",
"tn-ZA" => "Segoe UI",
"tr-TR" => "Segoe UI",
"tt-RU" => "Segoe UI",
"ug-CN" => "Segoe UI",
"uk-UA" => "Segoe UI",
"ur-PK" => "Segoe UI",
"uz-Latn-UZ" => "Segoe UI",
"vi-VN" => "Segoe UI",
"wo-SN" => "Segoe UI",
"xh-ZA" => "Segoe UI",
"yo-NG" => "Segoe UI",
"zh-CN" => "Microsoft YaHei UI",
"zh-HK" => "Microsoft JhengHei UI",
"zh-TW" => "Microsoft JhengHei UI",
"zh-Hans" => "Microsoft YaHei UI",
"zh-Hant" => "Microsoft YaHei UI",
"zu-ZA" => "Segoe UI",
_ => null,
};
if (familyName is null)
continue;
var family = IFontFamilyId
.ListSystemFonts(false)
.FirstOrDefault(
x => x.EnglishName.Equals(familyName, StringComparison.InvariantCultureIgnoreCase));
if (family?.Fonts[family.FindBestMatch(weight, stretch, style)] is not { } font)
return;
font.AddToBuildToolkit(this, fontConfig with { MergeFont = targetFont });
return;
}
}
/// <inheritdoc/>
public void AttachExtraGlyphsForDalamudLanguage(in SafeFontConfig fontConfig)
{
var targetFont = fontConfig.MergeFont;
if (targetFont.IsNull)
targetFont = this.Font;
if (targetFont.IsNull)
return;
var dalamudConfiguration = Service<DalamudConfiguration>.Get();
if (dalamudConfiguration.EffectiveLanguage == "ko"
|| Service<DalamudIme>.GetNullable()?.EncounteredHangul is true)
{
this.AddDalamudAssetFont(
DalamudAsset.NotoSansKrRegular,
fontConfig with
{
MergeFont = targetFont,
GlyphRanges = default(FluentGlyphRangeBuilder).WithLanguage("ko-kr").BuildExact(),
});
}
if (Service<DalamudConfiguration>.Get().EffectiveLanguage == "tw")
{
this.AttachWindowsDefaultFont(CultureInfo.GetCultureInfo("zh-hant"), fontConfig with
{
GlyphRanges = default(FluentGlyphRangeBuilder).WithLanguage("zh-hant").BuildExact(),
});
}
else if (Service<DalamudConfiguration>.Get().EffectiveLanguage == "zh"
|| Service<DalamudIme>.GetNullable()?.EncounteredHan is true)
{
this.AttachWindowsDefaultFont(CultureInfo.GetCultureInfo("zh-hans"), fontConfig with
{
GlyphRanges = default(FluentGlyphRangeBuilder).WithLanguage("zh-hans").BuildExact(),
});
}
}
public void PreBuildSubstances()
{
foreach (var substance in this.data.Substances)
substance.OnPreBuild(this);
foreach (var substance in this.data.Substances)
substance.OnPreBuildCleanup(this);
}
public unsafe void PreBuild()
{
var configData = this.data.ConfigData;
foreach (ref var config in configData.DataSpan)
{
if (this.GetFontScaleMode(config.DstFont) != FontScaleMode.Default)
continue;
config.SizePixels *= this.Scale;
config.GlyphMaxAdvanceX *= this.Scale;
if (float.IsInfinity(config.GlyphMaxAdvanceX) || float.IsNaN(config.GlyphMaxAdvanceX))
config.GlyphMaxAdvanceX = config.GlyphMaxAdvanceX > 0 ? float.MaxValue : -float.MaxValue;
config.GlyphMinAdvanceX *= this.Scale;
if (float.IsInfinity(config.GlyphMinAdvanceX) || float.IsNaN(config.GlyphMinAdvanceX))
config.GlyphMinAdvanceX = config.GlyphMinAdvanceX > 0 ? float.MaxValue : -float.MaxValue;
config.GlyphOffset *= this.Scale;
}
}
public void DoBuild()
{
// ImGui will call AddFontDefault() on Build() call.
// AddFontDefault() will reliably crash, when invoked multithreaded.
// We add a dummy font to prevent that.
if (this.data.ConfigData.Length == 0)
{
this.AddDalamudAssetFont(
DalamudAsset.NotoSansJpMedium,
new() { GlyphRanges = new ushort[] { ' ', ' ', '\0' }, SizePx = 1 });
}
if (!this.NewImAtlas.Build())
throw new InvalidOperationException("ImFontAtlas.Build failed");
this.BuildStep = FontAtlasBuildStep.PostBuild;
}
public unsafe void PostBuild()
{
var scale = this.Scale;
foreach (ref var font in this.Fonts.DataSpan)
{
if (this.GetFontScaleMode(font) != FontScaleMode.SkipHandling)
font.AdjustGlyphMetrics(1 / scale, 1 / scale);
foreach (var c in FallbackCodepoints)
{
var g = font.FindGlyphNoFallback(c);
if (g == null)
continue;
font.UpdateFallbackChar(c);
break;
}
foreach (var c in EllipsisCodepoints)
{
var g = font.FindGlyphNoFallback(c);
if (g == null)
continue;
font.EllipsisChar = c;
break;
}
}
}
public void PostBuildSubstances()
{
foreach (var substance in this.data.Substances)
substance.OnPostBuild(this);
}
public void PostBuildCallbacks()
{
foreach (var ac in this.registeredPostBuildActions)
ac.InvokeSafely();
this.registeredPostBuildActions.Clear();
}
public unsafe void UploadTextures()
{
var buf = Array.Empty<byte>();
try
{
var use4 = this.factory.TextureManager.IsDxgiFormatSupported(DXGI_FORMAT.DXGI_FORMAT_B4G4R4A4_UNORM);
var bpp = use4 ? 2 : 4;
var width = this.NewImAtlas.TexWidth;
var height = this.NewImAtlas.TexHeight;
var textureSpan = this.data.ImTextures.DataSpan;
for (var i = 0; i < textureSpan.Length; i++)
{
ref var texture = ref textureSpan[i];
var name =
$"{nameof(FontAtlasBuiltData)}[{this.data.Owner?.Name ?? "-"}][0x{(long)this.data.Atlas.Handle:X}][{i}]";
if (!texture.TexID.IsNull)
{
// Nothing to do
}
else if (texture.TexPixelsRGBA32 is not null)
{
var wrap = this.factory.TextureManager.CreateFromRaw(
RawImageSpecification.Rgba32(width, height),
new(texture.TexPixelsRGBA32, width * height * 4),
name);
this.factory.TextureManager.Blame(wrap, this.data.Owner?.OwnerPlugin);
this.data.AddExistingTexture(wrap);
texture.TexID = wrap.Handle;
}
else if (texture.TexPixelsAlpha8 is not null)
{
var numPixels = width * height;
if (buf.Length < numPixels * bpp)
{
ArrayPool<byte>.Shared.Return(buf);
buf = ArrayPool<byte>.Shared.Rent(numPixels * bpp);
}
fixed (void* pBuf = buf)
{
var sourcePtr = texture.TexPixelsAlpha8;
if (use4)
{
var target = (ushort*)pBuf;
while (numPixels-- > 0)
{
*target = (ushort)((*sourcePtr << 8) | 0x0FFF);
target++;
sourcePtr++;
}
}
else
{
var target = (uint*)pBuf;
while (numPixels-- > 0)
{
*target = (uint)((*sourcePtr << 24) | 0x00FFFFFF);
target++;
sourcePtr++;
}
}
}
var wrap = this.factory.TextureManager.CreateFromRaw(
new(
width,
height,
(int)(use4 ? DXGI_FORMAT.DXGI_FORMAT_B4G4R4A4_UNORM : DXGI_FORMAT.DXGI_FORMAT_B8G8R8A8_UNORM),
width * bpp),
buf,
name);
this.factory.TextureManager.Blame(wrap, this.data.Owner?.OwnerPlugin);
this.data.AddExistingTexture(wrap);
texture.TexID = wrap.Handle;
continue;
}
else
{
Log.Warning(
"[{name}]: TexID, TexPixelsRGBA32, and TexPixelsAlpha8 are all null",
this.data.Owner?.Name ?? "(error)");
}
if (texture.TexPixelsRGBA32 is not null)
ImGui.MemFree(texture.TexPixelsRGBA32);
if (texture.TexPixelsAlpha8 is not null)
ImGui.MemFree(texture.TexPixelsAlpha8);
texture.TexPixelsRGBA32 = null;
texture.TexPixelsAlpha8 = null;
}
}
finally
{
ArrayPool<byte>.Shared.Return(buf);
}
}
/// <inheritdoc/>
public unsafe void CopyGlyphsAcrossFonts(
ImFontPtr source,
ImFontPtr target,
bool missingOnly,
bool rebuildLookupTable = true,
char rangeLow = ' ',
char rangeHigh = '\uFFFE')
{
var sourceFound = false;
var targetFound = false;
foreach (var f in this.Fonts)
{
sourceFound |= f.Handle == source.Handle;
targetFound |= f.Handle == target.Handle;
}
if (sourceFound && targetFound)
{
ImGuiHelpers.CopyGlyphsAcrossFonts(
source,
target,
missingOnly,
false,
rangeLow,
rangeHigh);
if (rebuildLookupTable)
this.BuildLookupTable(target);
}
}
/// <inheritdoc/>
public unsafe void BuildLookupTable(ImFontPtr font)
{
// Need to clear previous Fallback pointers before BuildLookupTable, or it may crash
font.Handle->FallbackGlyph = null;
font.Handle->FallbackHotData = null;
font.BuildLookupTable();
// Need to fix our custom ImGui, so that imgui_widgets.cpp:3656 stops thinking
// Codepoint < FallbackHotData.size always means that it's not fallback char.
// Otherwise, having a fallback character in ImGui.InputText gets strange.
var indexedHotData = font.IndexedHotDataWrapped();
var indexLookup = font.IndexLookupWrapped();
ref var fallbackHotData = ref *(ImGuiHelpers.ImFontGlyphHotDataReal*)font.Handle->FallbackHotData;
for (var codepoint = 0; codepoint < indexedHotData.Length; codepoint++)
{
if (indexLookup[codepoint] == ushort.MaxValue)
{
indexedHotData[codepoint].AdvanceX = fallbackHotData.AdvanceX;
indexedHotData[codepoint].OccupiedWidth = fallbackHotData.OccupiedWidth;
}
}
}
/// <inheritdoc/>
public void FitRatio(ImFontPtr font, bool rebuildLookupTable = true)
{
var nsize = font.FontSize;
var glyphs = font.GlyphsWrapped();
foreach (ref var glyph in glyphs.DataSpan)
{
var ratio = 1f;
if (glyph.X1 - glyph.X0 > nsize)
ratio = Math.Max(ratio, (glyph.X1 - glyph.X0) / nsize);
if (glyph.Y1 - glyph.Y0 > nsize)
ratio = Math.Max(ratio, (glyph.Y1 - glyph.Y0) / nsize);
var w = MathF.Round((glyph.X1 - glyph.X0) / ratio, MidpointRounding.ToZero);
var h = MathF.Round((glyph.Y1 - glyph.Y0) / ratio, MidpointRounding.AwayFromZero);
glyph.X0 = MathF.Round((nsize - w) / 2f, MidpointRounding.ToZero);
glyph.Y0 = MathF.Round((nsize - h) / 2f, MidpointRounding.AwayFromZero);
glyph.X1 = glyph.X0 + w;
glyph.Y1 = glyph.Y0 + h;
glyph.AdvanceX = nsize;
}
if (rebuildLookupTable)
this.BuildLookupTable(font);
}
}
}