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