From cc756c243cfd9adeec02c4842a650a9bbd9137ac Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Wed, 28 Feb 2024 21:04:57 +0900 Subject: [PATCH] Add ITextureProvider.GetFromManifestResource(Assembly,string) --- .../ManifestResourceSharedImmediateTexture.cs | 64 ++++++++ Dalamud/Interface/Internal/TextureManager.cs | 126 ++++++++++++---- .../Windows/Data/Widgets/TexWidget.cs | 140 +++++++++++++++++- Dalamud/Plugin/Services/ITextureProvider.cs | 7 + Dalamud/Utility/TexFileExtensions.cs | 25 ++++ 5 files changed, 327 insertions(+), 35 deletions(-) create mode 100644 Dalamud/Interface/Internal/SharedImmediateTextures/ManifestResourceSharedImmediateTexture.cs diff --git a/Dalamud/Interface/Internal/SharedImmediateTextures/ManifestResourceSharedImmediateTexture.cs b/Dalamud/Interface/Internal/SharedImmediateTextures/ManifestResourceSharedImmediateTexture.cs new file mode 100644 index 000000000..de55a18f5 --- /dev/null +++ b/Dalamud/Interface/Internal/SharedImmediateTextures/ManifestResourceSharedImmediateTexture.cs @@ -0,0 +1,64 @@ +using System.IO; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; + +using Dalamud.Utility; + +namespace Dalamud.Interface.Internal.SharedImmediateTextures; + +/// Represents a sharable texture, based on a manifest texture obtained from +/// . +internal sealed class ManifestResourceSharedImmediateTexture : SharedImmediateTexture +{ + private readonly Assembly assembly; + private readonly string name; + + /// Initializes a new instance of the class. + /// The assembly containing manifest resources. + /// The case-sensitive name of the manifest resource being requested. + private ManifestResourceSharedImmediateTexture(Assembly assembly, string name) + { + this.assembly = assembly; + this.name = name; + } + + /// + public override string SourcePathForDebug => $"{this.assembly.GetName().FullName}:{this.name}"; + + /// Creates a new placeholder instance of . + /// The arguments to pass to the constructor. + /// The new instance. + public static SharedImmediateTexture CreatePlaceholder((Assembly Assembly, string Name) args) => + new ManifestResourceSharedImmediateTexture(args.Assembly, args.Name); + + /// + public override string ToString() => + $"{nameof(ManifestResourceSharedImmediateTexture)}#{this.InstanceIdForDebug}({this.SourcePathForDebug})"; + + /// + protected override void ReleaseResources() + { + _ = this.UnderlyingWrap?.ToContentDisposedTask(true); + this.UnderlyingWrap = null; + } + + /// + protected override void ReviveResources() => + this.UnderlyingWrap = Service.Get().CreateLoader( + this, + this.CreateTextureAsync, + this.LoadCancellationToken); + + private async Task CreateTextureAsync(CancellationToken cancellationToken) + { + await using var stream = this.assembly.GetManifestResourceStream(this.name); + if (stream is null) + throw new FileNotFoundException("The resource file could not be found."); + + var tm = await Service.GetAsync(); + var ms = new MemoryStream(stream.CanSeek ? (int)stream.Length : 0); + await stream.CopyToAsync(ms, cancellationToken); + return tm.NoThrottleGetFromImage(ms.GetBuffer().AsMemory(0, (int)ms.Length)); + } +} diff --git a/Dalamud/Interface/Internal/TextureManager.cs b/Dalamud/Interface/Internal/TextureManager.cs index 3f44bcf20..018e55d8b 100644 --- a/Dalamud/Interface/Internal/TextureManager.cs +++ b/Dalamud/Interface/Internal/TextureManager.cs @@ -2,6 +2,7 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.IO; +using System.Reflection; using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; @@ -17,6 +18,7 @@ using Dalamud.Logging.Internal; using Dalamud.Plugin.Services; using Dalamud.Utility; +using Lumina.Data; using Lumina.Data.Files; using SharpDX; @@ -59,8 +61,14 @@ internal sealed class TextureManager : IServiceType, IDisposable, ITextureProvid private readonly TextureLoadThrottler textureLoadThrottler = Service.Get(); private readonly ConcurrentLru lookupToPath = new(PathLookupLruCount); + private readonly ConcurrentDictionary gamePathTextures = new(); + private readonly ConcurrentDictionary fileSystemTextures = new(); + + private readonly ConcurrentDictionary<(Assembly Assembly, string Name), SharedImmediateTexture> + manifestResourceTextures = new(); + private readonly HashSet invalidatedTextures = new(); private bool disposing; @@ -71,12 +79,15 @@ internal sealed class TextureManager : IServiceType, IDisposable, ITextureProvid /// public event ITextureSubstitutionProvider.TextureDataInterceptorDelegate? InterceptTexDataLoad; - /// Gets all the loaded textures from the game resources. + /// Gets all the loaded textures from game resources. public ICollection GamePathTexturesForDebug => this.gamePathTextures.Values; - /// Gets all the loaded textures from the game resources. + /// Gets all the loaded textures from filesystem. public ICollection FileSystemTexturesForDebug => this.fileSystemTextures.Values; + /// Gets all the loaded textures from assembly manifest resources. + public ICollection ManifestResourceTexturesForDebug => this.manifestResourceTextures.Values; + /// Gets all the loaded textures that are invalidated from . /// lock on use of the value returned from this property. [SuppressMessage( @@ -92,14 +103,20 @@ internal sealed class TextureManager : IServiceType, IDisposable, ITextureProvid return; this.disposing = true; - foreach (var v in this.gamePathTextures.Values) - v.ReleaseSelfReference(true); - foreach (var v in this.fileSystemTextures.Values) - v.ReleaseSelfReference(true); + ReleaseSelfReferences(this.gamePathTextures); + ReleaseSelfReferences(this.fileSystemTextures); + ReleaseSelfReferences(this.manifestResourceTextures); this.lookupToPath.Clear(); - this.gamePathTextures.Clear(); - this.fileSystemTextures.Clear(); + + return; + + static void ReleaseSelfReferences(ConcurrentDictionary dict) + { + foreach (var v in dict.Values) + v.ReleaseSelfReference(true); + dict.Clear(); + } } #region API9 compat @@ -157,13 +174,29 @@ internal sealed class TextureManager : IServiceType, IDisposable, ITextureProvid /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public SharedImmediateTexture GetFromGame(string path) => - this.gamePathTextures.GetOrAdd(path, GamePathSharedImmediateTexture.CreatePlaceholder); + public SharedImmediateTexture GetFromGame(string path) + { + ObjectDisposedException.ThrowIf(this.disposing, this); + return this.gamePathTextures.GetOrAdd(path, GamePathSharedImmediateTexture.CreatePlaceholder); + } /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public SharedImmediateTexture GetFromFile(string path) => - this.fileSystemTextures.GetOrAdd(path, FileSystemSharedImmediateTexture.CreatePlaceholder); + public SharedImmediateTexture GetFromFile(string path) + { + ObjectDisposedException.ThrowIf(this.disposing, this); + return this.fileSystemTextures.GetOrAdd(path, FileSystemSharedImmediateTexture.CreatePlaceholder); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public SharedImmediateTexture GetFromManifestResource(Assembly assembly, string name) + { + ObjectDisposedException.ThrowIf(this.disposing, this); + return this.manifestResourceTextures.GetOrAdd( + (assembly, name), + ManifestResourceSharedImmediateTexture.CreatePlaceholder); + } /// [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -177,6 +210,11 @@ internal sealed class TextureManager : IServiceType, IDisposable, ITextureProvid [MethodImpl(MethodImplOptions.AggressiveInlining)] ISharedImmediateTexture ITextureProvider.GetFromFile(string path) => this.GetFromFile(path); + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + ISharedImmediateTexture ITextureProvider.GetFromManifestResource(Assembly assembly, string name) => + this.GetFromManifestResource(assembly, name); + /// public Task CreateFromImageAsync( ReadOnlyMemory bytes, @@ -433,15 +471,39 @@ internal sealed class TextureManager : IServiceType, IDisposable, ITextureProvid /// The loaded texture. internal IDalamudTextureWrap NoThrottleGetFromImage(ReadOnlyMemory bytes) { + ObjectDisposedException.ThrowIf(this.disposing, this); + if (this.interfaceManager.Scene is not { } scene) { _ = Service.Get(); scene = this.interfaceManager.Scene ?? throw new InvalidOperationException(); } + var bytesArray = bytes.ToArray(); + var texFileAttemptException = default(Exception); + if (TexFileExtensions.IsPossiblyTexFile2D(bytesArray)) + { + var tf = new TexFile(); + typeof(TexFile).GetProperty(nameof(tf.Data))!.GetSetMethod(true)!.Invoke( + tf, + new object?[] { bytesArray }); + typeof(TexFile).GetProperty(nameof(tf.Reader))!.GetSetMethod(true)!.Invoke( + tf, + new object?[] { new LuminaBinaryReader(bytesArray) }); + // Note: FileInfo and FilePath are not used from TexFile; skip it. + try + { + return this.NoThrottleGetFromTexFile(tf); + } + catch (Exception e) + { + texFileAttemptException = e; + } + } + return new DalamudTextureWrap( - scene.LoadImage(bytes.ToArray()) - ?? throw new("Failed to load image because of an unknown reason.")); + scene.LoadImage(bytesArray) + ?? throw texFileAttemptException ?? new("Failed to load image because of an unknown reason.")); } /// Gets a texture from the given . Skips the load throttler; intended to be used from @@ -450,6 +512,8 @@ internal sealed class TextureManager : IServiceType, IDisposable, ITextureProvid /// The loaded texture. internal IDalamudTextureWrap NoThrottleGetFromTexFile(TexFile file) { + ObjectDisposedException.ThrowIf(this.disposing, this); + var buffer = file.TextureBuffer; var (dxgiFormat, conversion) = TexFile.GetDxgiFormatFromTextureFormat(file.Header.Format, false); if (conversion != TexFile.DxgiFormatConversion.NoConversion || !this.SupportsDxgiFormat(dxgiFormat)) @@ -476,23 +540,9 @@ internal sealed class TextureManager : IServiceType, IDisposable, ITextureProvid private void FrameworkOnUpdate(IFramework unused) { - if (!this.gamePathTextures.IsEmpty) - { - foreach (var (k, v) in this.gamePathTextures) - { - if (TextureFinalReleasePredicate(v)) - _ = this.gamePathTextures.TryRemove(k, out _); - } - } - - if (!this.fileSystemTextures.IsEmpty) - { - foreach (var (k, v) in this.fileSystemTextures) - { - if (TextureFinalReleasePredicate(v)) - _ = this.fileSystemTextures.TryRemove(k, out _); - } - } + RemoveFinalReleased(this.gamePathTextures); + RemoveFinalReleased(this.fileSystemTextures); + RemoveFinalReleased(this.manifestResourceTextures); // ReSharper disable once InconsistentlySynchronizedField if (this.invalidatedTextures.Count != 0) @@ -503,6 +553,20 @@ internal sealed class TextureManager : IServiceType, IDisposable, ITextureProvid return; + [MethodImpl(MethodImplOptions.AggressiveInlining)] + static void RemoveFinalReleased(ConcurrentDictionary dict) + { + if (!dict.IsEmpty) + { + foreach (var (k, v) in dict) + { + if (TextureFinalReleasePredicate(v)) + _ = dict.TryRemove(k, out _); + } + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] static bool TextureFinalReleasePredicate(SharedImmediateTexture v) => v.ContentQueried && v.ReleaseSelfReference(false) == 0 && !v.HasRevivalPossibility; } diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs index c75a0c629..980f32f3c 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs @@ -1,6 +1,9 @@ using System.Collections.Generic; using System.IO; +using System.Linq; using System.Numerics; +using System.Reflection; +using System.Runtime.Loader; using System.Threading.Tasks; using Dalamud.Interface.Components; @@ -27,6 +30,11 @@ internal class TexWidget : IDataWindowWidget private bool hq = false; private string inputTexPath = string.Empty; private string inputFilePath = string.Empty; + private Assembly[]? inputManifestResourceAssemblyCandidates; + private string[]? inputManifestResourceAssemblyCandidateNames; + private int inputManifestResourceAssemblyIndex; + private string[]? inputManifestResourceNameCandidates; + private int inputManifestResourceNameIndex; private Vector2 inputTexUv0 = Vector2.Zero; private Vector2 inputTexUv1 = Vector2.One; private Vector4 inputTintCol = Vector4.One; @@ -53,6 +61,11 @@ internal class TexWidget : IDataWindowWidget this.inputFilePath = Path.Join( Service.Get().StartInfo.AssetDirectory!, DalamudAsset.Logo.GetAttribute()!.FileName); + this.inputManifestResourceAssemblyCandidates = null; + this.inputManifestResourceAssemblyCandidateNames = null; + this.inputManifestResourceAssemblyIndex = 0; + this.inputManifestResourceNameCandidates = null; + this.inputManifestResourceNameIndex = 0; this.Ready = true; } @@ -65,19 +78,28 @@ internal class TexWidget : IDataWindowWidget GC.Collect(); ImGui.PushID("loadedGameTextures"); - if (ImGui.CollapsingHeader($"Loaded Game Textures: {this.textureManager.GamePathTexturesForDebug.Count:g}###header")) + if (ImGui.CollapsingHeader( + $"Loaded Game Textures: {this.textureManager.GamePathTexturesForDebug.Count:g}###header")) this.DrawLoadedTextures(this.textureManager.GamePathTexturesForDebug); ImGui.PopID(); ImGui.PushID("loadedFileTextures"); - if (ImGui.CollapsingHeader($"Loaded File Textures: {this.textureManager.FileSystemTexturesForDebug.Count:g}###header")) + if (ImGui.CollapsingHeader( + $"Loaded File Textures: {this.textureManager.FileSystemTexturesForDebug.Count:g}###header")) this.DrawLoadedTextures(this.textureManager.FileSystemTexturesForDebug); ImGui.PopID(); + ImGui.PushID("loadedManifestResourceTextures"); + if (ImGui.CollapsingHeader( + $"Loaded Manifest Resource Textures: {this.textureManager.ManifestResourceTexturesForDebug.Count:g}###header")) + this.DrawLoadedTextures(this.textureManager.ManifestResourceTexturesForDebug); + ImGui.PopID(); + lock (this.textureManager.InvalidatedTexturesForDebug) { ImGui.PushID("invalidatedTextures"); - if (ImGui.CollapsingHeader($"Invalidated: {this.textureManager.InvalidatedTexturesForDebug.Count:g}###header")) + if (ImGui.CollapsingHeader( + $"Invalidated: {this.textureManager.InvalidatedTexturesForDebug.Count:g}###header")) { this.DrawLoadedTextures(this.textureManager.InvalidatedTexturesForDebug); } @@ -86,16 +108,39 @@ internal class TexWidget : IDataWindowWidget } if (ImGui.CollapsingHeader("Load Game File by Icon ID", ImGuiTreeNodeFlags.DefaultOpen)) + { + ImGui.PushID(nameof(this.DrawIconInput)); this.DrawIconInput(); + ImGui.PopID(); + } if (ImGui.CollapsingHeader("Load Game File by Path", ImGuiTreeNodeFlags.DefaultOpen)) + { + ImGui.PushID(nameof(this.DrawGamePathInput)); this.DrawGamePathInput(); + ImGui.PopID(); + } if (ImGui.CollapsingHeader("Load File", ImGuiTreeNodeFlags.DefaultOpen)) + { + ImGui.PushID(nameof(this.DrawFileInput)); this.DrawFileInput(); + ImGui.PopID(); + } + + if (ImGui.CollapsingHeader("Load Assembly Manifest Resource", ImGuiTreeNodeFlags.DefaultOpen)) + { + ImGui.PushID(nameof(this.DrawAssemblyManifestResourceInput)); + this.DrawAssemblyManifestResourceInput(); + ImGui.PopID(); + } if (ImGui.CollapsingHeader("UV")) + { + ImGui.PushID(nameof(this.DrawUvInput)); this.DrawUvInput(); + ImGui.PopID(); + } TextureEntry? toRemove = null; TextureEntry? toCopy = null; @@ -337,6 +382,81 @@ internal class TexWidget : IDataWindowWidget ImGuiHelpers.ScaledDummy(10); } + private void DrawAssemblyManifestResourceInput() + { + if (this.inputManifestResourceAssemblyCandidateNames is null || + this.inputManifestResourceAssemblyCandidates is null) + { + this.inputManifestResourceAssemblyIndex = 0; + this.inputManifestResourceAssemblyCandidates = + AssemblyLoadContext + .All + .SelectMany(x => x.Assemblies) + .Distinct() + .OrderBy(x => x.GetName().FullName) + .ToArray(); + this.inputManifestResourceAssemblyCandidateNames = + this.inputManifestResourceAssemblyCandidates + .Select(x => x.GetName().FullName) + .ToArray(); + } + + if (ImGui.Combo( + "Assembly", + ref this.inputManifestResourceAssemblyIndex, + this.inputManifestResourceAssemblyCandidateNames, + this.inputManifestResourceAssemblyCandidateNames.Length)) + { + this.inputManifestResourceNameIndex = 0; + this.inputManifestResourceNameCandidates = null; + } + + var assembly = + this.inputManifestResourceAssemblyIndex >= 0 + && this.inputManifestResourceAssemblyIndex < this.inputManifestResourceAssemblyCandidates.Length + ? this.inputManifestResourceAssemblyCandidates[this.inputManifestResourceAssemblyIndex] + : null; + + this.inputManifestResourceNameCandidates ??= assembly?.GetManifestResourceNames() ?? Array.Empty(); + + ImGui.Combo( + "Name", + ref this.inputManifestResourceNameIndex, + this.inputManifestResourceNameCandidates, + this.inputManifestResourceNameCandidates.Length); + + var name = + this.inputManifestResourceNameIndex >= 0 + && this.inputManifestResourceNameIndex < this.inputManifestResourceNameCandidates.Length + ? this.inputManifestResourceNameCandidates[this.inputManifestResourceNameIndex] + : null; + + if (ImGui.Button("Refresh Assemblies")) + { + this.inputManifestResourceAssemblyIndex = 0; + this.inputManifestResourceAssemblyCandidates = null; + this.inputManifestResourceAssemblyCandidateNames = null; + this.inputManifestResourceNameIndex = 0; + this.inputManifestResourceNameCandidates = null; + } + + if (assembly is not null && name is not null) + { + ImGui.SameLine(); + if (ImGui.Button("Load File (Async)")) + { + this.addedTextures.Add( + new(Api10: this.textureManager.GetFromManifestResource(assembly, name).RentAsync())); + } + + ImGui.SameLine(); + if (ImGui.Button("Load File (Immediate)")) + this.addedTextures.Add(new(Api10ImmManifestResource: (assembly, name))); + } + + ImGuiHelpers.ScaledDummy(10); + } + private void DrawUvInput() { ImGui.InputFloat2("UV0", ref this.inputTexUv0); @@ -389,7 +509,8 @@ internal class TexWidget : IDataWindowWidget Task? Api10 = null, GameIconLookup? Api10ImmGameIcon = null, string? Api10ImmGamePath = null, - string? Api10ImmFile = null) : IDisposable + string? Api10ImmFile = null, + (Assembly Assembly, string Name)? Api10ImmManifestResource = null) : IDisposable { private static int idCounter; @@ -421,6 +542,8 @@ internal class TexWidget : IDataWindowWidget return "Must not happen"; if (this.Api10ImmFile is not null) return "Must not happen"; + if (this.Api10ImmManifestResource is not null) + return "Must not happen"; return "Not implemented"; } @@ -438,6 +561,13 @@ internal class TexWidget : IDataWindowWidget return tp.GetFromGame(this.Api10ImmGamePath).GetWrap(); if (this.Api10ImmFile is not null) return tp.GetFromFile(this.Api10ImmFile).GetWrap(); + if (this.Api10ImmManifestResource is not null) + { + return tp.GetFromManifestResource( + this.Api10ImmManifestResource.Value.Assembly, + this.Api10ImmManifestResource.Value.Name).GetWrap(); + } + return null; } @@ -460,6 +590,8 @@ internal class TexWidget : IDataWindowWidget return $"{nameof(this.Api10ImmGamePath)}: {this.Api10ImmGamePath}"; if (this.Api10ImmFile is not null) return $"{nameof(this.Api10ImmFile)}: {this.Api10ImmFile}"; + if (this.Api10ImmManifestResource is not null) + return $"{nameof(this.Api10ImmManifestResource)}: {this.Api10ImmManifestResource}"; return "Not implemented"; } } diff --git a/Dalamud/Plugin/Services/ITextureProvider.cs b/Dalamud/Plugin/Services/ITextureProvider.cs index c63e7ae4f..031825379 100644 --- a/Dalamud/Plugin/Services/ITextureProvider.cs +++ b/Dalamud/Plugin/Services/ITextureProvider.cs @@ -1,5 +1,6 @@ using System.Diagnostics.CodeAnalysis; using System.IO; +using System.Reflection; using System.Threading; using System.Threading.Tasks; @@ -41,6 +42,12 @@ public partial interface ITextureProvider /// The shared texture that you may use to obtain the loaded texture wrap and load states. ISharedImmediateTexture GetFromFile(string path); + /// Gets a shared texture corresponding to the given file of the assembly manifest resources. + /// The assembly containing manifest resources. + /// The case-sensitive name of the manifest resource being requested. + /// The shared texture that you may use to obtain the loaded texture wrap and load states. + ISharedImmediateTexture GetFromManifestResource(Assembly assembly, string name); + /// Gets a texture from the given bytes, trying to interpret it as a .tex file or other well-known image /// files, such as .png. /// The bytes to load. diff --git a/Dalamud/Utility/TexFileExtensions.cs b/Dalamud/Utility/TexFileExtensions.cs index 5abea692a..ec8e10b3c 100644 --- a/Dalamud/Utility/TexFileExtensions.cs +++ b/Dalamud/Utility/TexFileExtensions.cs @@ -1,3 +1,7 @@ +using System.Runtime.CompilerServices; + +using Dalamud.Memory; + using ImGuiScene; using Lumina.Data.Files; @@ -28,4 +32,25 @@ public static class TexFileExtensions return dst; } + + /// Determines if the given data is possibly a . + /// The data. + /// true if it should be attempted to be interpreted as a . + internal static unsafe bool IsPossiblyTexFile2D(ReadOnlySpan data) + { + if (data.Length < Unsafe.SizeOf()) + return false; + fixed (byte* ptr = data) + { + ref readonly var texHeader = ref MemoryHelper.Cast((nint)ptr); + if ((texHeader.Type & TexFile.Attribute.TextureTypeMask) != TexFile.Attribute.TextureType2D) + return false; + if (!Enum.IsDefined(texHeader.Format)) + return false; + if (texHeader.Width == 0 || texHeader.Height == 0) + return false; + } + + return true; + } }