using System.Buffers;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Dalamud.Configuration.Internal;
using Dalamud.Data;
using Dalamud.Game;
using Dalamud.Interface.GameFonts;
using Dalamud.Interface.Internal;
using Dalamud.Storage.Assets;
using Dalamud.Utility;
using ImGuiNET;
using ImGuiScene;
using Lumina.Data.Files;
using SharpDX;
using SharpDX.Direct3D11;
using SharpDX.DXGI;
namespace Dalamud.Interface.ManagedFontAtlas.Internals;
///
/// Factory for the implementation of .
///
[ServiceManager.BlockingEarlyLoadedService]
internal sealed partial class FontAtlasFactory
: IServiceType, GamePrebakedFontHandle.IGameFontTextureProvider, IDisposable
{
private readonly DisposeSafety.ScopedFinalizer scopedFinalizer = new();
private readonly CancellationTokenSource cancellationTokenSource = new();
private readonly IReadOnlyDictionary> fdtFiles;
private readonly IReadOnlyDictionary[]>> texFiles;
private readonly IReadOnlyDictionary> prebakedTextureWraps;
private readonly Task defaultGlyphRanges;
private readonly DalamudAssetManager dalamudAssetManager;
[ServiceManager.ServiceConstructor]
private FontAtlasFactory(
DataManager dataManager,
Framework framework,
InterfaceManager interfaceManager,
DalamudAssetManager dalamudAssetManager)
{
this.Framework = framework;
this.InterfaceManager = interfaceManager;
this.dalamudAssetManager = dalamudAssetManager;
this.SceneTask = Service
.GetAsync()
.ContinueWith(r => r.Result.Manager.Scene);
var gffasInfo = Enum.GetValues()
.Select(
x =>
(
Font: x,
Attr: x.GetAttribute()))
.Where(x => x.Attr is not null)
.ToArray();
var texPaths = gffasInfo.Select(x => x.Attr.TexPathFormat).Distinct().ToArray();
this.fdtFiles = gffasInfo.ToImmutableDictionary(
x => x.Font,
x => Task.Run(() => dataManager.GetFile(x.Attr.Path)!.Data));
var channelCountsTask = texPaths.ToImmutableDictionary(
x => x,
x => Task.WhenAll(
gffasInfo.Where(y => y.Attr.TexPathFormat == x)
.Select(y => this.fdtFiles[y.Font]))
.ContinueWith(
files => 1 + files.Result.Max(
file =>
{
unsafe
{
using var pin = file.AsMemory().Pin();
var fdt = new FdtFileView(pin.Pointer, file.Length);
return fdt.MaxTextureIndex;
}
})));
this.prebakedTextureWraps = channelCountsTask.ToImmutableDictionary(
x => x.Key,
x => x.Value.ContinueWith(y => new IDalamudTextureWrap?[y.Result]));
this.texFiles = channelCountsTask.ToImmutableDictionary(
x => x.Key,
x => x.Value.ContinueWith(
y => Enumerable
.Range(1, 1 + ((y.Result - 1) / 4))
.Select(z => Task.Run(() => dataManager.GetFile(string.Format(x.Key, z))!))
.ToArray()));
this.defaultGlyphRanges =
this.fdtFiles[GameFontFamilyAndSize.Axis12]
.ContinueWith(
file =>
{
unsafe
{
using var pin = file.Result.AsMemory().Pin();
var fdt = new FdtFileView(pin.Pointer, file.Result.Length);
return fdt.ToGlyphRanges();
}
});
}
///
/// Gets or sets a value indicating whether to override configuration for UseAxis.
///
public bool? UseAxisOverride { get; set; } = null;
///
/// Gets a value indicating whether to use AXIS fonts.
///
public bool UseAxis => this.UseAxisOverride ?? Service.Get().UseAxisFontsFromGame;
///
/// Gets the service instance of .
///
public Framework Framework { get; }
///
/// Gets the service instance of .
/// may not yet be available.
///
public InterfaceManager InterfaceManager { get; }
///
/// Gets the async task for inside .
///
public Task SceneTask { get; }
///
/// Gets the default glyph ranges (glyph ranges of ).
///
public ushort[] DefaultGlyphRanges => ExtractResult(this.defaultGlyphRanges);
///
/// Gets a value indicating whether game symbol font file is available.
///
public bool HasGameSymbolsFontFile =>
this.dalamudAssetManager.IsStreamImmediatelyAvailable(DalamudAsset.LodestoneGameSymbol);
///
public void Dispose()
{
this.cancellationTokenSource.Cancel();
this.scopedFinalizer.Dispose();
this.cancellationTokenSource.Dispose();
}
///
/// Creates a new instance of a class that implements the interface.
///
/// Name of atlas, for debugging and logging purposes.
/// Specify how to auto rebuild.
/// Whether the fonts in the atlas is global scaled.
/// The new font atlas.
public IFontAtlas CreateFontAtlas(
string atlasName,
FontAtlasAutoRebuildMode autoRebuildMode,
bool isGlobalScaled = true) =>
new DalamudFontAtlas(this, atlasName, autoRebuildMode, isGlobalScaled);
///
/// Adds the font from Dalamud Assets.
///
/// The toolkitPostBuild.
/// The font.
/// The font config.
/// The address and size.
public ImFontPtr AddFont(
IFontAtlasBuildToolkitPreBuild toolkitPreBuild,
DalamudAsset asset,
in SafeFontConfig fontConfig) =>
toolkitPreBuild.AddFontFromStream(
this.dalamudAssetManager.CreateStream(asset),
fontConfig,
false,
$"Asset({asset})");
///
/// Gets the for the .
///
/// The font family and size.
/// The .
public FdtReader GetFdtReader(GameFontFamilyAndSize gffas) => new(ExtractResult(this.fdtFiles[gffas]));
///
public unsafe MemoryHandle CreateFdtFileView(GameFontFamilyAndSize gffas, out FdtFileView fdtFileView)
{
var arr = ExtractResult(this.fdtFiles[gffas]);
var handle = arr.AsMemory().Pin();
try
{
fdtFileView = new(handle.Pointer, arr.Length);
return handle;
}
catch
{
handle.Dispose();
throw;
}
}
///
public int GetFontTextureCount(string texPathFormat) =>
ExtractResult(this.prebakedTextureWraps[texPathFormat]).Length;
///
public TexFile GetTexFile(string texPathFormat, int index) =>
ExtractResult(ExtractResult(this.texFiles[texPathFormat])[index]);
///
public IDalamudTextureWrap NewFontTextureRef(string texPathFormat, int textureIndex)
{
lock (this.prebakedTextureWraps[texPathFormat])
{
var wraps = ExtractResult(this.prebakedTextureWraps[texPathFormat]);
var fileIndex = textureIndex / 4;
var channelIndex = FdtReader.FontTableEntry.TextureChannelOrder[textureIndex % 4];
wraps[textureIndex] ??= this.GetChannelTexture(texPathFormat, fileIndex, channelIndex);
return CloneTextureWrap(wraps[textureIndex]);
}
}
private static T ExtractResult(Task t) => t.IsCompleted ? t.Result : t.GetAwaiter().GetResult();
private static unsafe void ExtractChannelFromB8G8R8A8(
Span target,
ReadOnlySpan source,
int channelIndex,
bool targetIsB4G4R4A4)
{
var numPixels = Math.Min(source.Length / 4, target.Length / (targetIsB4G4R4A4 ? 2 : 4));
fixed (byte* sourcePtrImmutable = source)
{
var rptr = sourcePtrImmutable + channelIndex;
fixed (void* targetPtr = target)
{
if (targetIsB4G4R4A4)
{
var wptr = (ushort*)targetPtr;
while (numPixels-- > 0)
{
*wptr = (ushort)((*rptr << 8) | 0x0FFF);
wptr++;
rptr += 4;
}
}
else
{
var wptr = (uint*)targetPtr;
while (numPixels-- > 0)
{
*wptr = (uint)((*rptr << 24) | 0x00FFFFFF);
wptr++;
rptr += 4;
}
}
}
}
}
///
/// Clones a texture wrap, by getting a new reference to the underlying and the
/// texture behind.
///
/// The to clone from.
/// The cloned .
private static IDalamudTextureWrap CloneTextureWrap(IDalamudTextureWrap wrap)
{
var srv = CppObject.FromPointer(wrap.ImGuiHandle);
using var res = srv.Resource;
using var tex2D = res.QueryInterface();
var description = tex2D.Description;
return new DalamudTextureWrap(
new D3DTextureWrap(
srv.QueryInterface(),
description.Width,
description.Height));
}
private static unsafe void ExtractChannelFromB4G4R4A4(
Span target,
ReadOnlySpan source,
int channelIndex,
bool targetIsB4G4R4A4)
{
var numPixels = Math.Min(source.Length / 2, target.Length / (targetIsB4G4R4A4 ? 2 : 4));
fixed (byte* sourcePtrImmutable = source)
{
var rptr = sourcePtrImmutable + (channelIndex / 2);
var rshift = (channelIndex & 1) == 0 ? 0 : 4;
fixed (void* targetPtr = target)
{
if (targetIsB4G4R4A4)
{
var wptr = (ushort*)targetPtr;
while (numPixels-- > 0)
{
*wptr = (ushort)(((*rptr >> rshift) << 12) | 0x0FFF);
wptr++;
rptr += 2;
}
}
else
{
var wptr = (uint*)targetPtr;
while (numPixels-- > 0)
{
var v = (*rptr >> rshift) & 0xF;
v |= v << 4;
*wptr = (uint)((v << 24) | 0x00FFFFFF);
wptr++;
rptr += 4;
}
}
}
}
}
private IDalamudTextureWrap GetChannelTexture(string texPathFormat, int fileIndex, int channelIndex)
{
var texFile = ExtractResult(ExtractResult(this.texFiles[texPathFormat])[fileIndex]);
var numPixels = texFile.Header.Width * texFile.Header.Height;
_ = Service.Get();
var targetIsB4G4R4A4 = this.InterfaceManager.SupportsDxgiFormat(Format.B4G4R4A4_UNorm);
var bpp = targetIsB4G4R4A4 ? 2 : 4;
var buffer = ArrayPool.Shared.Rent(numPixels * bpp);
try
{
var sliceSpan = texFile.SliceSpan(0, 0, out _, out _, out _);
switch (texFile.Header.Format)
{
case TexFile.TextureFormat.B4G4R4A4:
// Game ships with this format.
ExtractChannelFromB4G4R4A4(buffer, sliceSpan, channelIndex, targetIsB4G4R4A4);
break;
case TexFile.TextureFormat.B8G8R8A8:
// In case of modded font textures.
ExtractChannelFromB8G8R8A8(buffer, sliceSpan, channelIndex, targetIsB4G4R4A4);
break;
default:
// Unlikely.
ExtractChannelFromB8G8R8A8(buffer, texFile.ImageData, channelIndex, targetIsB4G4R4A4);
break;
}
return this.scopedFinalizer.Add(
this.InterfaceManager.LoadImageFromDxgiFormat(
buffer,
texFile.Header.Width * bpp,
texFile.Header.Width,
texFile.Header.Height,
targetIsB4G4R4A4 ? Format.B4G4R4A4_UNorm : Format.B8G8R8A8_UNorm));
}
finally
{
ArrayPool.Shared.Return(buffer);
}
}
}