Add ITextureProvider.GetFromManifestResource(Assembly,string)

This commit is contained in:
Soreepeong 2024-02-28 21:04:57 +09:00
parent b52d4724e9
commit cc756c243c
5 changed files with 327 additions and 35 deletions

View file

@ -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;
/// <summary>Represents a sharable texture, based on a manifest texture obtained from
/// <see cref="Assembly.GetManifestResourceStream(string)"/>.</summary>
internal sealed class ManifestResourceSharedImmediateTexture : SharedImmediateTexture
{
private readonly Assembly assembly;
private readonly string name;
/// <summary>Initializes a new instance of the <see cref="ManifestResourceSharedImmediateTexture"/> class.</summary>
/// <param name="assembly">The assembly containing manifest resources.</param>
/// <param name="name">The case-sensitive name of the manifest resource being requested.</param>
private ManifestResourceSharedImmediateTexture(Assembly assembly, string name)
{
this.assembly = assembly;
this.name = name;
}
/// <inheritdoc/>
public override string SourcePathForDebug => $"{this.assembly.GetName().FullName}:{this.name}";
/// <summary>Creates a new placeholder instance of <see cref="ManifestResourceSharedImmediateTexture"/>.</summary>
/// <param name="args">The arguments to pass to the constructor.</param>
/// <returns>The new instance.</returns>
public static SharedImmediateTexture CreatePlaceholder((Assembly Assembly, string Name) args) =>
new ManifestResourceSharedImmediateTexture(args.Assembly, args.Name);
/// <inheritdoc/>
public override string ToString() =>
$"{nameof(ManifestResourceSharedImmediateTexture)}#{this.InstanceIdForDebug}({this.SourcePathForDebug})";
/// <inheritdoc/>
protected override void ReleaseResources()
{
_ = this.UnderlyingWrap?.ToContentDisposedTask(true);
this.UnderlyingWrap = null;
}
/// <inheritdoc/>
protected override void ReviveResources() =>
this.UnderlyingWrap = Service<TextureLoadThrottler>.Get().CreateLoader(
this,
this.CreateTextureAsync,
this.LoadCancellationToken);
private async Task<IDalamudTextureWrap> 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<TextureManager>.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));
}
}

View file

@ -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<TextureLoadThrottler>.Get();
private readonly ConcurrentLru<GameIconLookup, string> lookupToPath = new(PathLookupLruCount);
private readonly ConcurrentDictionary<string, SharedImmediateTexture> gamePathTextures = new();
private readonly ConcurrentDictionary<string, SharedImmediateTexture> fileSystemTextures = new();
private readonly ConcurrentDictionary<(Assembly Assembly, string Name), SharedImmediateTexture>
manifestResourceTextures = new();
private readonly HashSet<SharedImmediateTexture> invalidatedTextures = new();
private bool disposing;
@ -71,12 +79,15 @@ internal sealed class TextureManager : IServiceType, IDisposable, ITextureProvid
/// <inheritdoc/>
public event ITextureSubstitutionProvider.TextureDataInterceptorDelegate? InterceptTexDataLoad;
/// <summary>Gets all the loaded textures from the game resources.</summary>
/// <summary>Gets all the loaded textures from game resources.</summary>
public ICollection<SharedImmediateTexture> GamePathTexturesForDebug => this.gamePathTextures.Values;
/// <summary>Gets all the loaded textures from the game resources.</summary>
/// <summary>Gets all the loaded textures from filesystem.</summary>
public ICollection<SharedImmediateTexture> FileSystemTexturesForDebug => this.fileSystemTextures.Values;
/// <summary>Gets all the loaded textures from assembly manifest resources.</summary>
public ICollection<SharedImmediateTexture> ManifestResourceTexturesForDebug => this.manifestResourceTextures.Values;
/// <summary>Gets all the loaded textures that are invalidated from <see cref="InvalidatePaths"/>.</summary>
/// <remarks><c>lock</c> on use of the value returned from this property.</remarks>
[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<T>(ConcurrentDictionary<T, SharedImmediateTexture> 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
/// <inheritdoc cref="ITextureProvider.GetFromGame"/>
[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);
}
/// <inheritdoc cref="ITextureProvider.GetFromFile"/>
[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);
}
/// <inheritdoc cref="ITextureProvider.GetFromFile"/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public SharedImmediateTexture GetFromManifestResource(Assembly assembly, string name)
{
ObjectDisposedException.ThrowIf(this.disposing, this);
return this.manifestResourceTextures.GetOrAdd(
(assembly, name),
ManifestResourceSharedImmediateTexture.CreatePlaceholder);
}
/// <inheritdoc/>
[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);
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
ISharedImmediateTexture ITextureProvider.GetFromManifestResource(Assembly assembly, string name) =>
this.GetFromManifestResource(assembly, name);
/// <inheritdoc/>
public Task<IDalamudTextureWrap> CreateFromImageAsync(
ReadOnlyMemory<byte> bytes,
@ -433,15 +471,39 @@ internal sealed class TextureManager : IServiceType, IDisposable, ITextureProvid
/// <returns>The loaded texture.</returns>
internal IDalamudTextureWrap NoThrottleGetFromImage(ReadOnlyMemory<byte> bytes)
{
ObjectDisposedException.ThrowIf(this.disposing, this);
if (this.interfaceManager.Scene is not { } scene)
{
_ = Service<InterfaceManager.InterfaceManagerWithScene>.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."));
}
/// <summary>Gets a texture from the given <see cref="TexFile"/>. Skips the load throttler; intended to be used from
@ -450,6 +512,8 @@ internal sealed class TextureManager : IServiceType, IDisposable, ITextureProvid
/// <returns>The loaded texture.</returns>
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<T>(ConcurrentDictionary<T, SharedImmediateTexture> 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;
}

View file

@ -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<Dalamud>.Get().StartInfo.AssetDirectory!,
DalamudAsset.Logo.GetAttribute<DalamudAssetPathAttribute>()!.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<string>();
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<IDalamudTextureWrap>? 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";
}
}

View file

@ -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
/// <returns>The shared texture that you may use to obtain the loaded texture wrap and load states.</returns>
ISharedImmediateTexture GetFromFile(string path);
/// <summary>Gets a shared texture corresponding to the given file of the assembly manifest resources.</summary>
/// <param name="assembly">The assembly containing manifest resources.</param>
/// <param name="name">The case-sensitive name of the manifest resource being requested.</param>
/// <returns>The shared texture that you may use to obtain the loaded texture wrap and load states.</returns>
ISharedImmediateTexture GetFromManifestResource(Assembly assembly, string name);
/// <summary>Gets a texture from the given bytes, trying to interpret it as a .tex file or other well-known image
/// files, such as .png.</summary>
/// <param name="bytes">The bytes to load.</param>

View file

@ -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;
}
/// <summary>Determines if the given data is possibly a <see cref="TexFile"/>.</summary>
/// <param name="data">The data.</param>
/// <returns><c>true</c> if it should be attempted to be interpreted as a <see cref="TexFile"/>.</returns>
internal static unsafe bool IsPossiblyTexFile2D(ReadOnlySpan<byte> data)
{
if (data.Length < Unsafe.SizeOf<TexFile.TexHeader>())
return false;
fixed (byte* ptr = data)
{
ref readonly var texHeader = ref MemoryHelper.Cast<TexFile.TexHeader>((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;
}
}