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); } } }