Implement DalamudFontAtlas

This commit is contained in:
Soreepeong 2023-11-21 15:09:38 +09:00 committed by Soreepeong
parent 01cde50a46
commit 8bdab4d2c8
41 changed files with 7551 additions and 1428 deletions

View file

@ -1,10 +1,15 @@
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Numerics;
using System.Reactive.Disposables;
using System.Runtime.InteropServices;
using System.Text.Unicode;
using Dalamud.Configuration.Internal;
using Dalamud.Game.ClientState.Keys;
using Dalamud.Interface.ManagedFontAtlas;
using Dalamud.Interface.ManagedFontAtlas.Internals;
using Dalamud.Interface.Utility.Raii;
using ImGuiNET;
using ImGuiScene;
@ -31,7 +36,7 @@ public static class ImGuiHelpers
/// This does not necessarily mean you can call drawing functions.
/// </summary>
public static unsafe bool IsImGuiInitialized =>
ImGui.GetCurrentContext() is not 0 && ImGui.GetIO().NativePtr is not null;
ImGui.GetCurrentContext() != nint.Zero && ImGui.GetIO().NativePtr is not null;
/// <summary>
/// Gets the global Dalamud scale; even available before drawing is ready.<br />
@ -342,25 +347,18 @@ public static class ImGuiHelpers
}
if (changed && rebuildLookupTable)
target.BuildLookupTableNonstandard();
}
{
// ImGui resolves ' ' with FindGlyph, which uses FallbackGlyph.
// FallbackGlyph is resolved after resolving ' '.
// On the first call of BuildLookupTable, called from BuildFonts, FallbackGlyph is set to null,
// making FindGlyph return nullptr.
// On our secondary calls of BuildLookupTable, FallbackGlyph is set to some value that is not null,
// making ImGui attempt to treat whatever was there as a ' '.
// This may cause random glyphs to be sized randomly, if not an access violation exception.
target.NativePtr->FallbackGlyph = null;
/// <summary>
/// Call ImFont::BuildLookupTable, after attempting to fulfill some preconditions.
/// </summary>
/// <param name="font">The font.</param>
public static unsafe void BuildLookupTableNonstandard(this ImFontPtr font)
{
// ImGui resolves ' ' with FindGlyph, which uses FallbackGlyph.
// FallbackGlyph is resolved after resolving ' '.
// On the first call of BuildLookupTable, called from BuildFonts, FallbackGlyph is set to null,
// making FindGlyph return nullptr.
// On our secondary calls of BuildLookupTable, FallbackGlyph is set to some value that is not null,
// making ImGui attempt to treat whatever was there as a ' '.
// This may cause random glyphs to be sized randomly, if not an access violation exception.
font.NativePtr->FallbackGlyph = null;
font.BuildLookupTable();
target.BuildLookupTable();
}
}
/// <summary>
@ -406,6 +404,129 @@ public static class ImGuiHelpers
public static void CenterCursorFor(float itemWidth) =>
ImGui.SetCursorPosX((int)((ImGui.GetWindowWidth() - itemWidth) / 2));
/// <summary>
/// Allocates memory on the heap using <see cref="ImGuiNative.igMemAlloc"/><br />
/// Memory must be freed using <see cref="ImGuiNative.igMemFree"/>.
/// <br />
/// Note that null is a valid return value when <paramref name="length"/> is 0.
/// </summary>
/// <param name="length">The length of allocated memory.</param>
/// <returns>The allocated memory.</returns>
/// <exception cref="OutOfMemoryException">If <see cref="ImGuiNative.igMemAlloc"/> returns null.</exception>
public static unsafe void* AllocateMemory(int length)
{
switch (length)
{
case 0:
return null;
case < 0:
throw new ArgumentOutOfRangeException(
nameof(length),
length,
$"{nameof(length)} cannot be a negative number.");
default:
var memory = ImGuiNative.igMemAlloc((uint)length);
if (memory is null)
{
throw new OutOfMemoryException(
$"Failed to allocate {length} bytes using {nameof(ImGuiNative.igMemAlloc)}");
}
return memory;
}
}
/// <summary>
/// Mark 4K page as used, after adding a codepoint to a font.
/// </summary>
/// <param name="font">The font.</param>
/// <param name="codepoint">The codepoint.</param>
public static unsafe void Mark4KPageUsedAfterGlyphAdd(this ImFontPtr font, ushort codepoint)
{
// Mark 4K page as used
var pageIndex = unchecked((ushort)(codepoint / 4096));
font.NativePtr->Used4kPagesMap[pageIndex >> 3] |= unchecked((byte)(1 << (pageIndex & 7)));
}
/// <summary>
/// Creates a new instance of <see cref="ImFontAtlasPtr"/> with a natively backed memory.
/// </summary>
/// <param name="font">The created instance.</param>
/// <returns>Disposable you can call.</returns>
public static unsafe IDisposable NewFontAtlasPtrScoped(out ImFontAtlasPtr font)
{
font = new(ImGuiNative.ImFontAtlas_ImFontAtlas());
var ptr = font.NativePtr;
return Disposable.Create(() =>
{
if (ptr != null)
ImGuiNative.ImFontAtlas_destroy(ptr);
ptr = null;
});
}
/// <summary>
/// Creates a new instance of <see cref="ImFontGlyphRangesBuilderPtr"/> with a natively backed memory.
/// </summary>
/// <param name="builder">The created instance.</param>
/// <returns>Disposable you can call.</returns>
public static unsafe IDisposable NewFontGlyphRangeBuilderPtrScoped(out ImFontGlyphRangesBuilderPtr builder)
{
builder = new(ImGuiNative.ImFontGlyphRangesBuilder_ImFontGlyphRangesBuilder());
var ptr = builder.NativePtr;
return Disposable.Create(() =>
{
if (ptr != null)
ImGuiNative.ImFontGlyphRangesBuilder_destroy(ptr);
ptr = null;
});
}
/// <summary>
/// Builds ImGui Glyph Ranges for use with <see cref="SafeFontConfig.GlyphRanges"/>.
/// </summary>
/// <param name="builder">The builder.</param>
/// <param name="addFallbackCodepoints">Add fallback codepoints to the range.</param>
/// <param name="addEllipsisCodepoints">Add ellipsis codepoints to the range.</param>
/// <returns>When disposed, the resource allocated for the range will be freed.</returns>
public static unsafe ushort[] BuildRangesToArray(
this ImFontGlyphRangesBuilderPtr builder,
bool addFallbackCodepoints = true,
bool addEllipsisCodepoints = true)
{
if (addFallbackCodepoints)
builder.AddText(FontAtlasFactory.FallbackCodepoints);
if (addEllipsisCodepoints)
{
builder.AddText(FontAtlasFactory.EllipsisCodepoints);
builder.AddChar('.');
}
builder.BuildRanges(out var vec);
return new ReadOnlySpan<ushort>((void*)vec.Data, vec.Size).ToArray();
}
/// <inheritdoc cref="CreateImGuiRangesFrom(IEnumerable{UnicodeRange})"/>
public static ushort[] CreateImGuiRangesFrom(params UnicodeRange[] ranges)
=> CreateImGuiRangesFrom((IEnumerable<UnicodeRange>)ranges);
/// <summary>
/// Creates glyph ranges from <see cref="UnicodeRange"/>.<br />
/// Use values from <see cref="UnicodeRanges"/>.
/// </summary>
/// <param name="ranges">The unicode ranges.</param>
/// <returns>The range array that can be used for <see cref="SafeFontConfig.GlyphRanges"/>.</returns>
public static ushort[] CreateImGuiRangesFrom(IEnumerable<UnicodeRange> ranges) =>
ranges
.Where(x => x.FirstCodePoint <= ushort.MaxValue)
.SelectMany(
x => new[]
{
(ushort)Math.Min(x.FirstCodePoint, ushort.MaxValue),
(ushort)Math.Min(x.FirstCodePoint + x.Length, ushort.MaxValue),
})
.ToArray();
/// <summary>
/// Determines whether <paramref name="ptr"/> is empty.
/// </summary>
@ -414,7 +535,7 @@ public static class ImGuiHelpers
public static unsafe bool IsNull(this ImFontPtr ptr) => ptr.NativePtr == null;
/// <summary>
/// Determines whether <paramref name="ptr"/> is not null and loaded.
/// Determines whether <paramref name="ptr"/> is empty.
/// </summary>
/// <param name="ptr">The pointer.</param>
/// <returns>Whether it is empty.</returns>
@ -447,6 +568,98 @@ public static class ImGuiHelpers
return -1;
}
/// <summary>
/// If <paramref name="self"/> is default, then returns <paramref name="other"/>.
/// </summary>
/// <param name="self">The self.</param>
/// <param name="other">The other.</param>
/// <returns><paramref name="self"/> if it is not default; otherwise, <paramref name="other"/>.</returns>
public static unsafe ImFontPtr OrElse(this ImFontPtr self, ImFontPtr other) =>
self.NativePtr is null ? other : self;
/// <summary>
/// Attempts to validate that <paramref name="fontPtr"/> is valid.
/// </summary>
/// <param name="fontPtr">The font pointer.</param>
/// <returns>The exception, if any occurred during validation.</returns>
internal static unsafe Exception? ValidateUnsafe(this ImFontPtr fontPtr)
{
try
{
var font = fontPtr.NativePtr;
if (font is null)
throw new NullReferenceException("The font is null.");
_ = Marshal.ReadIntPtr((nint)font);
if (font->IndexedHotData.Data != 0)
_ = Marshal.ReadIntPtr(font->IndexedHotData.Data);
if (font->FrequentKerningPairs.Data != 0)
_ = Marshal.ReadIntPtr(font->FrequentKerningPairs.Data);
if (font->IndexLookup.Data != 0)
_ = Marshal.ReadIntPtr(font->IndexLookup.Data);
if (font->Glyphs.Data != 0)
_ = Marshal.ReadIntPtr(font->Glyphs.Data);
if (font->KerningPairs.Data != 0)
_ = Marshal.ReadIntPtr(font->KerningPairs.Data);
if (font->ConfigDataCount == 0 && font->ConfigData is not null)
throw new InvalidOperationException("ConfigDataCount == 0 but ConfigData is not null?");
if (font->ConfigDataCount != 0 && font->ConfigData is null)
throw new InvalidOperationException("ConfigDataCount != 0 but ConfigData is null?");
if (font->ConfigData is not null)
_ = Marshal.ReadIntPtr((nint)font->ConfigData);
if (font->FallbackGlyph is not null
&& ((nint)font->FallbackGlyph < font->Glyphs.Data || (nint)font->FallbackGlyph >= font->Glyphs.Data))
throw new InvalidOperationException("FallbackGlyph is not in range of Glyphs.Data");
if (font->FallbackHotData is not null
&& ((nint)font->FallbackHotData < font->IndexedHotData.Data
|| (nint)font->FallbackHotData >= font->IndexedHotData.Data))
throw new InvalidOperationException("FallbackGlyph is not in range of Glyphs.Data");
if (font->ContainerAtlas is not null)
_ = Marshal.ReadIntPtr((nint)font->ContainerAtlas);
}
catch (Exception e)
{
return e;
}
return null;
}
/// <summary>
/// Updates the fallback char of <paramref name="font"/>.
/// </summary>
/// <param name="font">The font.</param>
/// <param name="c">The fallback character.</param>
internal static unsafe void UpdateFallbackChar(this ImFontPtr font, char c)
{
font.FallbackChar = c;
font.NativePtr->FallbackHotData =
(ImFontGlyphHotData*)((ImFontGlyphHotDataReal*)font.IndexedHotData.Data + font.FallbackChar);
}
/// <summary>
/// Determines if the supplied codepoint is inside the given range,
/// in format of <see cref="ImFontConfig.GlyphRanges"/>.
/// </summary>
/// <param name="codepoint">The codepoint.</param>
/// <param name="rangePtr">The ranges.</param>
/// <returns>Whether it is the case.</returns>
internal static unsafe bool IsCodepointInSuppliedGlyphRangesUnsafe(int codepoint, ushort* rangePtr)
{
if (codepoint is <= 0 or >= ushort.MaxValue)
return false;
while (*rangePtr != 0)
{
var from = *rangePtr++;
var to = *rangePtr++;
if (from <= codepoint && codepoint <= to)
return true;
}
return false;
}
/// <summary>
/// Get data needed for each new frame.
/// </summary>