diff --git a/Dalamud/Data/DataManager.cs b/Dalamud/Data/DataManager.cs
index f450175f7..a4c81c7a7 100644
--- a/Dalamud/Data/DataManager.cs
+++ b/Dalamud/Data/DataManager.cs
@@ -188,15 +188,17 @@ public sealed class DataManager : IDisposable, IServiceType, IDataManager
///
/// The icon ID.
/// The containing the icon.
- /// TODO(v9): remove in api9 in favor of GetIcon(uint iconId, bool highResolution)
+ [Obsolete("Use ITextureProvider instead")]
public TexFile? GetIcon(uint iconId)
=> this.GetIcon(this.Language, iconId, false);
///
+ [Obsolete("Use ITextureProvider instead")]
public TexFile? GetIcon(uint iconId, bool highResolution)
=> this.GetIcon(this.Language, iconId, highResolution);
///
+ [Obsolete("Use ITextureProvider instead")]
public TexFile? GetIcon(bool isHq, uint iconId)
{
var type = isHq ? "hq/" : string.Empty;
@@ -209,11 +211,12 @@ public sealed class DataManager : IDisposable, IServiceType, IDataManager
/// The requested language.
/// The icon ID.
/// The containing the icon.
- /// TODO(v9): remove in api9 in favor of GetIcon(ClientLanguage iconLanguage, uint iconId, bool highResolution)
+ [Obsolete("Use ITextureProvider instead")]
public TexFile? GetIcon(ClientLanguage iconLanguage, uint iconId)
=> this.GetIcon(iconLanguage, iconId, false);
///
+ [Obsolete("Use ITextureProvider instead")]
public TexFile? GetIcon(ClientLanguage iconLanguage, uint iconId, bool highResolution)
{
var type = iconLanguage switch
@@ -234,11 +237,12 @@ public sealed class DataManager : IDisposable, IServiceType, IDataManager
/// The type of the icon (e.g. 'hq' to get the HQ variant of an item icon).
/// The icon ID.
/// The containing the icon.
- /// TODO(v9): remove in api9 in favor of GetIcon(string? type, uint iconId, bool highResolution)
+ [Obsolete("Use ITextureProvider instead")]
public TexFile? GetIcon(string? type, uint iconId)
=> this.GetIcon(type, iconId, false);
///
+ [Obsolete("Use ITextureProvider instead")]
public TexFile? GetIcon(string? type, uint iconId, bool highResolution)
{
var format = highResolution ? HighResolutionIconFileFormat : IconFileFormat;
@@ -260,10 +264,12 @@ public sealed class DataManager : IDisposable, IServiceType, IDataManager
}
///
+ [Obsolete("Use ITextureProvider instead")]
public TexFile? GetHqIcon(uint iconId)
=> this.GetIcon(true, iconId);
///
+ [Obsolete("Use ITextureProvider instead")]
[return: NotNullIfNotNull(nameof(tex))]
public TextureWrap? GetImGuiTexture(TexFile? tex)
{
@@ -290,6 +296,7 @@ public sealed class DataManager : IDisposable, IServiceType, IDataManager
}
///
+ [Obsolete("Use ITextureProvider instead")]
public TextureWrap? GetImGuiTexture(string path)
=> this.GetImGuiTexture(this.GetFile(path));
@@ -299,26 +306,32 @@ public sealed class DataManager : IDisposable, IServiceType, IDataManager
/// The icon ID.
/// The containing the icon.
/// TODO(v9): remove in api9 in favor of GetImGuiTextureIcon(uint iconId, bool highResolution)
+ [Obsolete("Use ITextureProvider instead")]
public TextureWrap? GetImGuiTextureIcon(uint iconId)
=> this.GetImGuiTexture(this.GetIcon(iconId, false));
///
+ [Obsolete("Use ITextureProvider instead")]
public TextureWrap? GetImGuiTextureIcon(uint iconId, bool highResolution)
=> this.GetImGuiTexture(this.GetIcon(iconId, highResolution));
///
+ [Obsolete("Use ITextureProvider instead")]
public TextureWrap? GetImGuiTextureIcon(bool isHq, uint iconId)
=> this.GetImGuiTexture(this.GetIcon(isHq, iconId));
///
+ [Obsolete("Use ITextureProvider instead")]
public TextureWrap? GetImGuiTextureIcon(ClientLanguage iconLanguage, uint iconId)
=> this.GetImGuiTexture(this.GetIcon(iconLanguage, iconId));
///
+ [Obsolete("Use ITextureProvider instead")]
public TextureWrap? GetImGuiTextureIcon(string type, uint iconId)
=> this.GetImGuiTexture(this.GetIcon(type, iconId));
///
+ [Obsolete("Use ITextureProvider instead")]
public TextureWrap? GetImGuiTextureHqIcon(uint iconId)
=> this.GetImGuiTexture(this.GetHqIcon(iconId));
diff --git a/Dalamud/Interface/Internal/DalamudTextureWrap.cs b/Dalamud/Interface/Internal/DalamudTextureWrap.cs
index 97fb1dd0b..039873f1f 100644
--- a/Dalamud/Interface/Internal/DalamudTextureWrap.cs
+++ b/Dalamud/Interface/Internal/DalamudTextureWrap.cs
@@ -4,11 +4,19 @@ using ImGuiScene;
namespace Dalamud.Interface.Internal;
+///
+/// Base TextureWrap interface for all Dalamud-owned texture wraps.
+/// Used to avoid referencing ImGuiScene.
+///
+public interface IDalamudTextureWrap : TextureWrap
+{
+}
+
///
/// Safety harness for ImGuiScene textures that will defer destruction until
/// the end of the frame.
///
-public class DalamudTextureWrap : TextureWrap
+public class DalamudTextureWrap : IDalamudTextureWrap
{
private readonly TextureWrap wrappedWrap;
diff --git a/Dalamud/Interface/Internal/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs
index 42402ed97..841511f55 100644
--- a/Dalamud/Interface/Internal/InterfaceManager.cs
+++ b/Dalamud/Interface/Internal/InterfaceManager.cs
@@ -669,12 +669,19 @@ internal class InterfaceManager : IDisposable, IServiceType
var pRes = this.presentHook.Original(swapChain, syncInterval, presentFlags);
this.RenderImGui();
+ this.DisposeTextures();
return pRes;
}
this.RenderImGui();
+ this.DisposeTextures();
+ return this.presentHook.Original(swapChain, syncInterval, presentFlags);
+ }
+
+ private void DisposeTextures()
+ {
if (this.deferredDisposeTextures.Count > 0)
{
Log.Verbose("[IM] Disposing {Count} textures", this.deferredDisposeTextures.Count);
@@ -685,8 +692,6 @@ internal class InterfaceManager : IDisposable, IServiceType
this.deferredDisposeTextures.Clear();
}
-
- return this.presentHook.Original(swapChain, syncInterval, presentFlags);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
diff --git a/Dalamud/Interface/Internal/TextureManager.cs b/Dalamud/Interface/Internal/TextureManager.cs
new file mode 100644
index 000000000..4b2f1f362
--- /dev/null
+++ b/Dalamud/Interface/Internal/TextureManager.cs
@@ -0,0 +1,598 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Numerics;
+
+using Dalamud.Data;
+using Dalamud.Game;
+using Dalamud.IoC;
+using Dalamud.IoC.Internal;
+using Dalamud.Logging.Internal;
+using Dalamud.Plugin.Services;
+using ImGuiScene;
+using Lumina.Data.Files;
+
+namespace Dalamud.Interface.Internal;
+
+///
+/// Service responsible for loading and disposing ImGui texture wraps.
+///
+[PluginInterface]
+[InterfaceVersion("1.0")]
+[ServiceManager.BlockingEarlyLoadedService]
+#pragma warning disable SA1015
+[ResolveVia]
+#pragma warning restore SA1015
+internal class TextureManager : IDisposable, IServiceType, ITextureSubstitutionProvider
+{
+ private const string IconFileFormat = "ui/icon/{0:D3}000/{1}{2:D6}.tex";
+ private const string HighResolutionIconFileFormat = "ui/icon/{0:D3}000/{1}{2:D6}_hr1.tex";
+
+ private const uint MillisecondsEvictionTime = 2000;
+
+ private static readonly ModuleLog Log = new("TEXM");
+
+ private readonly Framework framework;
+ private readonly DataManager dataManager;
+ private readonly InterfaceManager im;
+ private readonly DalamudStartInfo startInfo;
+
+ private readonly Dictionary activeTextures = new();
+
+ private TextureWrap? fallbackTextureWrap;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Framework instance.
+ /// DataManager instance.
+ /// InterfaceManager instance.
+ /// DalamudStartInfo instance.
+ [ServiceManager.ServiceConstructor]
+ public TextureManager(Framework framework, DataManager dataManager, InterfaceManager im, DalamudStartInfo startInfo)
+ {
+ this.framework = framework;
+ this.dataManager = dataManager;
+ this.im = im;
+ this.startInfo = startInfo;
+
+ this.framework.Update += this.FrameworkOnUpdate;
+
+ Service.GetAsync().ContinueWith(_ => this.CreateFallbackTexture());
+ }
+
+ ///
+ public event ITextureSubstitutionProvider.TextureDataInterceptorDelegate? InterceptTexDataLoad;
+
+ ///
+ /// Get a texture handle for a specific icon.
+ ///
+ /// The ID of the icon to load.
+ /// Options to be considered when loading the icon.
+ ///
+ /// The language to be considered when loading the icon, if the icon has versions for multiple languages.
+ /// If null, default to the game's current language.
+ ///
+ ///
+ /// Prevent Dalamud from automatically unloading this icon to save memory. Usually does not need to be set.
+ ///
+ ///
+ /// Null, if the icon does not exist in the specified configuration, or a texture wrap that can be used
+ /// to render the icon.
+ ///
+ public TextureManagerTextureWrap? GetIcon(uint iconId, ITextureProvider.IconFlags flags = ITextureProvider.IconFlags.HiRes, ClientLanguage? language = null, bool keepAlive = false)
+ {
+ var path = this.GetIconPath(iconId, flags, language);
+ return path == null ? null : this.CreateWrap(path, keepAlive);
+ }
+
+ ///
+ /// Get a path for a specific icon's .tex file.
+ ///
+ /// The ID of the icon to look up.
+ /// Options to be considered when loading the icon.
+ ///
+ /// The language to be considered when loading the icon, if the icon has versions for multiple languages.
+ /// If null, default to the game's current language.
+ ///
+ ///
+ /// Null, if the icon does not exist in the specified configuration, or the path to the texture's .tex file,
+ /// which can be loaded via IDataManager.
+ ///
+ public string? GetIconPath(uint iconId, ITextureProvider.IconFlags flags = ITextureProvider.IconFlags.HiRes, ClientLanguage? language = null)
+ {
+ var hiRes = flags.HasFlag(ITextureProvider.IconFlags.HiRes);
+
+ // 1. Item
+ var path = FormatIconPath(
+ iconId,
+ flags.HasFlag(ITextureProvider.IconFlags.ItemHighQuality) ? "hq/" : string.Empty,
+ hiRes);
+ if (this.dataManager.FileExists(path))
+ return path;
+
+ language ??= this.startInfo.Language;
+ var languageFolder = language switch
+ {
+ ClientLanguage.Japanese => "ja/",
+ ClientLanguage.English => "en/",
+ ClientLanguage.German => "de/",
+ ClientLanguage.French => "fr/",
+ _ => throw new ArgumentOutOfRangeException(nameof(language), $"Unknown Language: {language}"),
+ };
+
+ // 2. Regular icon, with language, hi-res
+ path = FormatIconPath(
+ iconId,
+ languageFolder,
+ hiRes);
+ if (this.dataManager.FileExists(path))
+ return path;
+
+ if (hiRes)
+ {
+ // 3. Regular icon, with language, no hi-res
+ path = FormatIconPath(
+ iconId,
+ languageFolder,
+ false);
+ if (this.dataManager.FileExists(path))
+ return path;
+ }
+
+ // 4. Regular icon, without language, hi-res
+ path = FormatIconPath(
+ iconId,
+ null,
+ hiRes);
+ if (this.dataManager.FileExists(path))
+ return path;
+
+ // 4. Regular icon, without language, no hi-res
+ if (hiRes)
+ {
+ path = FormatIconPath(
+ iconId,
+ null,
+ false);
+ if (this.dataManager.FileExists(path))
+ return path;
+ }
+
+ return null;
+ }
+
+ ///
+ /// Get a texture handle for the texture at the specified path.
+ /// You may only specify paths in the game's VFS.
+ ///
+ /// The path to the texture in the game's VFS.
+ /// Prevent Dalamud from automatically unloading this texture to save memory. Usually does not need to be set.
+ /// Null, if the icon does not exist, or a texture wrap that can be used to render the texture.
+ public TextureManagerTextureWrap? GetTextureFromGame(string path, bool keepAlive = false)
+ {
+ ArgumentException.ThrowIfNullOrEmpty(path);
+
+ if (Path.IsPathRooted(path))
+ throw new ArgumentException("Use GetTextureFromFile() to load textures directly from a file.", nameof(path));
+
+ return !this.dataManager.FileExists(path) ? null : this.CreateWrap(path, keepAlive);
+ }
+
+ ///
+ /// Get a texture handle for the image or texture, specified by the passed FileInfo.
+ /// You may only specify paths on the native file system.
+ ///
+ /// This API can load .png and .tex files.
+ ///
+ /// The FileInfo describing the image or texture file.
+ /// Prevent Dalamud from automatically unloading this texture to save memory. Usually does not need to be set.
+ /// Null, if the file does not exist, or a texture wrap that can be used to render the texture.
+ public TextureManagerTextureWrap? GetTextureFromFile(FileInfo file, bool keepAlive = false)
+ {
+ ArgumentNullException.ThrowIfNull(file);
+ return !file.Exists ? null : this.CreateWrap(file.FullName, keepAlive);
+ }
+
+ ///
+ /// Get a texture handle for the specified Lumina TexFile.
+ ///
+ /// The texture to obtain a handle to.
+ /// A texture wrap that can be used to render the texture.
+ public IDalamudTextureWrap? GetTexture(TexFile file)
+ {
+ ArgumentNullException.ThrowIfNull(file);
+
+ if (!this.im.IsReady)
+ throw new InvalidOperationException("Cannot create textures before scene is ready");
+
+#pragma warning disable CS0618
+ return this.dataManager.GetImGuiTexture(file) as IDalamudTextureWrap;
+#pragma warning restore CS0618
+ }
+
+ ///
+ public void Dispose()
+ {
+ this.fallbackTextureWrap?.Dispose();
+ this.framework.Update -= this.FrameworkOnUpdate;
+
+ Log.Verbose("Disposing {Num} left behind textures.");
+
+ foreach (var activeTexture in this.activeTextures)
+ {
+ activeTexture.Value.Wrap?.Dispose();
+ }
+
+ this.activeTextures.Clear();
+ }
+
+ ///
+ /// Get texture info.
+ ///
+ /// Path to the texture.
+ /// Whether or not the texture should be reloaded if it was unloaded.
+ ///
+ /// If true, exceptions caused by texture load will not be caught.
+ /// If false, exceptions will be caught and a dummy texture will be returned to prevent plugins from using invalid texture handles.
+ ///
+ /// Info object storing texture metadata.
+ internal TextureInfo GetInfo(string path, bool refresh = true, bool rethrow = false)
+ {
+ TextureInfo? info;
+ lock (this.activeTextures)
+ {
+ this.activeTextures.TryGetValue(path, out info);
+ }
+
+ if (info == null)
+ {
+ info = new TextureInfo();
+ lock (this.activeTextures)
+ {
+ if (!this.activeTextures.TryAdd(path, info))
+ Log.Warning("Texture {Path} tracked twice", path);
+ }
+ }
+
+ if (refresh && info.KeepAliveCount == 0)
+ info.LastAccess = DateTime.UtcNow;
+
+ if (info is { Wrap: not null })
+ return info;
+
+ if (refresh)
+ {
+ if (!this.im.IsReady)
+ throw new InvalidOperationException("Cannot create textures before scene is ready");
+
+ string? interceptPath = null;
+ this.InterceptTexDataLoad?.Invoke(path, ref interceptPath);
+
+ if (interceptPath != null)
+ {
+ Log.Verbose("Intercept: {OriginalPath} => {ReplacePath}", path, interceptPath);
+ path = interceptPath;
+ }
+
+ TextureWrap? wrap;
+ try
+ {
+ // We want to load this from the disk, probably, if the path has a root
+ // Not sure if this can cause issues with e.g. network drives, might have to rethink
+ // and add a flag instead if it does.
+ if (Path.IsPathRooted(path))
+ {
+ if (Path.GetExtension(path) == ".tex")
+ {
+ // Attempt to load via Lumina
+ var file = this.dataManager.GameData.GetFileFromDisk(path);
+ wrap = this.GetTexture(file);
+ Log.Verbose("Texture {Path} loaded FS via Lumina", path);
+ }
+ else
+ {
+ // Attempt to load image
+ wrap = this.im.LoadImage(path);
+ Log.Verbose("Texture {Path} loaded FS via LoadImage", path);
+ }
+ }
+ else
+ {
+ // Load regularly from dats
+ var file = this.dataManager.GetFile(path);
+ if (file == null)
+ throw new Exception("Could not load TexFile from dat.");
+
+ wrap = this.GetTexture(file);
+ Log.Verbose("Texture {Path} loaded from SqPack", path);
+ }
+
+ if (wrap == null)
+ throw new Exception("Could not create texture");
+
+ // TODO: We could support this, but I don't think it's worth it at the moment.
+ var extents = new Vector2(wrap.Width, wrap.Height);
+ if (info.Extents != Vector2.Zero && info.Extents != extents)
+ Log.Warning("Texture at {Path} changed size between reloads, this is currently not supported.", path);
+
+ info.Extents = extents;
+ }
+ catch (Exception e)
+ {
+ Log.Error(e, "Could not load texture from {Path}", path);
+
+ // When creating the texture initially, we want to be able to pass errors back to the plugin
+ if (rethrow)
+ throw;
+
+ // This means that the load failed due to circumstances outside of our control,
+ // and we can't do anything about it. Return a dummy texture so that the plugin still
+ // has something to draw.
+ wrap = this.fallbackTextureWrap;
+ }
+
+ info.Wrap = wrap;
+ }
+
+ return info;
+ }
+
+ ///
+ /// Notify the system about an instance of a texture wrap being disposed.
+ /// If required conditions are met, the texture will be unloaded at the next update.
+ ///
+ /// The path to the texture.
+ /// Whether or not this handle was created in keep-alive mode.
+ internal void NotifyTextureDisposed(string path, bool keepAlive)
+ {
+ var info = this.GetInfo(path, false);
+ info.RefCount--;
+
+ if (keepAlive)
+ info.KeepAliveCount--;
+
+ // Clean it up by the next update. If it's re-requested in-between, we don't reload it.
+ if (info.RefCount <= 0)
+ info.LastAccess = default;
+ }
+
+ private static string FormatIconPath(uint iconId, string? type, bool highResolution)
+ {
+ var format = highResolution ? HighResolutionIconFileFormat : IconFileFormat;
+
+ type ??= string.Empty;
+ if (type.Length > 0 && !type.EndsWith("/"))
+ type += "/";
+
+ return string.Format(format, iconId / 1000, type, iconId);
+ }
+
+ private TextureManagerTextureWrap? CreateWrap(string path, bool keepAlive)
+ {
+ // This will create the texture.
+ // That's fine, it's probably used immediately and this will let the plugin catch load errors.
+ var info = this.GetInfo(path, rethrow: true);
+ info.RefCount++;
+
+ if (keepAlive)
+ info.KeepAliveCount++;
+
+ return new TextureManagerTextureWrap(path, info.Extents, keepAlive, this);
+ }
+
+ private void FrameworkOnUpdate(Framework fw)
+ {
+ lock (this.activeTextures)
+ {
+ var toRemove = new List();
+
+ foreach (var texInfo in this.activeTextures)
+ {
+ if (texInfo.Value.RefCount == 0)
+ {
+ Log.Verbose("Evicting {Path} since no refs", texInfo.Key);
+
+ Debug.Assert(texInfo.Value.KeepAliveCount == 0, "texInfo.Value.KeepAliveCount == 0");
+
+ texInfo.Value.Wrap?.Dispose();
+ texInfo.Value.Wrap = null;
+ toRemove.Add(texInfo.Key);
+ continue;
+ }
+
+ if (texInfo.Value.KeepAliveCount > 0 || texInfo.Value.Wrap == null)
+ continue;
+
+ if (DateTime.UtcNow - texInfo.Value.LastAccess > TimeSpan.FromMilliseconds(MillisecondsEvictionTime))
+ {
+ Log.Verbose("Evicting {Path} since too old", texInfo.Key);
+ texInfo.Value.Wrap.Dispose();
+ texInfo.Value.Wrap = null;
+ }
+ }
+
+ foreach (var path in toRemove)
+ {
+ this.activeTextures.Remove(path);
+ }
+ }
+ }
+
+ private void CreateFallbackTexture()
+ {
+ var fallbackTexBytes = new byte[] { 0xFF, 0x00, 0xDC, 0xFF };
+ this.fallbackTextureWrap = this.im.LoadImageRaw(fallbackTexBytes, 1, 1, 4);
+ Debug.Assert(this.fallbackTextureWrap != null, "this.fallbackTextureWrap != null");
+ }
+
+ ///
+ /// Internal representation of a managed texture.
+ ///
+ internal class TextureInfo
+ {
+ ///
+ /// Gets or sets the actual texture wrap. May be unpopulated.
+ ///
+ public TextureWrap? Wrap { get; set; }
+
+ ///
+ /// Gets or sets the time the texture was last accessed.
+ ///
+ public DateTime LastAccess { get; set; }
+
+ ///
+ /// Gets or sets the number of active holders of this texture.
+ ///
+ public uint RefCount { get; set; }
+
+ ///
+ /// Gets or sets the number of active holders that want this texture to stay alive forever.
+ ///
+ public uint KeepAliveCount { get; set; }
+
+ ///
+ /// Gets or sets the extents of the texture.
+ ///
+ public Vector2 Extents { get; set; }
+ }
+}
+
+///
+/// Plugin-scoped version of a texture manager.
+///
+[PluginInterface]
+[InterfaceVersion("1.0")]
+[ServiceManager.ScopedService]
+#pragma warning disable SA1015
+[ResolveVia]
+#pragma warning restore SA1015
+internal class TextureManagerPluginScoped : ITextureProvider, IServiceType, IDisposable
+{
+ private readonly TextureManager textureManager;
+
+ private readonly List trackedTextures = new();
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// TextureManager instance.
+ public TextureManagerPluginScoped(TextureManager textureManager)
+ {
+ this.textureManager = textureManager;
+ }
+
+ ///
+ public IDalamudTextureWrap? GetIcon(
+ uint iconId,
+ ITextureProvider.IconFlags flags = ITextureProvider.IconFlags.ItemHighQuality,
+ ClientLanguage? language = null,
+ bool keepAlive = false)
+ {
+ var wrap = this.textureManager.GetIcon(iconId, flags, language, keepAlive);
+ if (wrap == null)
+ return null;
+
+ this.trackedTextures.Add(wrap);
+ return wrap;
+ }
+
+ ///
+ public string? GetIconPath(uint iconId, ITextureProvider.IconFlags flags = ITextureProvider.IconFlags.HiRes, ClientLanguage? language = null)
+ => this.textureManager.GetIconPath(iconId, flags, language);
+
+ ///
+ public IDalamudTextureWrap? GetTextureFromGame(string path, bool keepAlive = false)
+ {
+ ArgumentException.ThrowIfNullOrEmpty(path);
+
+ var wrap = this.textureManager.GetTextureFromGame(path, keepAlive);
+ if (wrap == null)
+ return null;
+
+ this.trackedTextures.Add(wrap);
+ return wrap;
+ }
+
+ ///
+ public IDalamudTextureWrap? GetTextureFromFile(FileInfo file, bool keepAlive)
+ {
+ ArgumentNullException.ThrowIfNull(file);
+
+ var wrap = this.textureManager.GetTextureFromFile(file, keepAlive);
+ if (wrap == null)
+ return null;
+
+ this.trackedTextures.Add(wrap);
+ return wrap;
+ }
+
+ ///
+ public IDalamudTextureWrap? GetTexture(TexFile file)
+ => this.textureManager.GetTexture(file);
+
+ ///
+ public void Dispose()
+ {
+ // Dispose all leaked textures
+ foreach (var textureWrap in this.trackedTextures.Where(x => !x.IsDisposed))
+ {
+ textureWrap.Dispose();
+ }
+ }
+}
+
+///
+/// Wrap.
+///
+internal class TextureManagerTextureWrap : IDalamudTextureWrap
+{
+ private readonly TextureManager manager;
+ private readonly string path;
+ private readonly bool keepAlive;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The path to the texture.
+ /// The extents of the texture.
+ /// Keep alive or not.
+ /// Manager that we obtained this from.
+ internal TextureManagerTextureWrap(string path, Vector2 extents, bool keepAlive, TextureManager manager)
+ {
+ this.path = path;
+ this.keepAlive = keepAlive;
+ this.manager = manager;
+ this.Width = (int)extents.X;
+ this.Height = (int)extents.Y;
+ }
+
+ ///
+ public IntPtr ImGuiHandle => this.manager.GetInfo(this.path).Wrap!.ImGuiHandle;
+
+ ///
+ public int Width { get; private set; }
+
+ ///
+ public int Height { get; private set; }
+
+ ///
+ /// Gets a value indicating whether or not this wrap has already been disposed.
+ /// If true, the handle may be invalid.
+ ///
+ internal bool IsDisposed { get; private set; }
+
+ ///
+ public void Dispose()
+ {
+ lock (this)
+ {
+ if (!this.IsDisposed)
+ this.manager.NotifyTextureDisposed(this.path, this.keepAlive);
+
+ this.IsDisposed = true;
+ }
+ }
+}
diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs
index 4a0cfe21a..5ad5868c3 100644
--- a/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs
+++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs
@@ -1,8 +1,9 @@
using System;
+using System.Collections.Generic;
+using System.IO;
using System.Numerics;
-using Dalamud.Data;
-using Dalamud.Utility;
+using Dalamud.Plugin.Services;
using ImGuiNET;
using ImGuiScene;
using Serilog;
@@ -14,13 +15,18 @@ namespace Dalamud.Interface.Internal.Windows.Data;
///
internal class TexWidget : IDataWindowWidget
{
+ private readonly List addedTextures = new();
+
+ private string iconId = "18";
+ private bool hiRes = true;
+ private bool hq = false;
+ private bool keepAlive = false;
private string inputTexPath = string.Empty;
- private TextureWrap? debugTex;
private Vector2 inputTexUv0 = Vector2.Zero;
private Vector2 inputTexUv1 = Vector2.One;
private Vector4 inputTintCol = Vector4.One;
private Vector2 inputTexScale = Vector2.Zero;
-
+
///
public DataKind DataKind { get; init; } = DataKind.Tex;
@@ -36,34 +42,87 @@ internal class TexWidget : IDataWindowWidget
///
public void Draw()
{
- var dataManager = Service.Get();
+ var texManager = Service.Get();
- ImGui.InputText("Tex Path", ref this.inputTexPath, 255);
- ImGui.InputFloat2("UV0", ref this.inputTexUv0);
- ImGui.InputFloat2("UV1", ref this.inputTexUv1);
- ImGui.InputFloat4("Tint", ref this.inputTintCol);
- ImGui.InputFloat2("Scale", ref this.inputTexScale);
-
- if (ImGui.Button("Load Tex"))
+ ImGui.InputText("Icon ID", ref this.iconId, 32);
+ ImGui.Checkbox("HQ Item", ref this.hq);
+ ImGui.Checkbox("Hi-Res", ref this.hiRes);
+ ImGui.Checkbox("Keep alive", ref this.keepAlive);
+ if (ImGui.Button("Load Icon"))
{
try
{
- this.debugTex = dataManager.GetImGuiTexture(this.inputTexPath);
- this.inputTexScale = new Vector2(this.debugTex?.Width ?? 0, this.debugTex?.Height ?? 0);
+ var flags = ITextureProvider.IconFlags.None;
+ if (this.hq)
+ flags |= ITextureProvider.IconFlags.ItemHighQuality;
+
+ if (this.hiRes)
+ flags |= ITextureProvider.IconFlags.HiRes;
+
+ this.addedTextures.Add(texManager.GetIcon(uint.Parse(this.iconId), flags, keepAlive: this.keepAlive));
}
catch (Exception ex)
{
Log.Error(ex, "Could not load tex");
}
}
+
+ ImGui.Separator();
+ ImGui.InputText("Tex Path", ref this.inputTexPath, 255);
+ if (ImGui.Button("Load Tex"))
+ {
+ try
+ {
+ this.addedTextures.Add(texManager.GetTextureFromGame(this.inputTexPath, this.keepAlive));
+ }
+ catch (Exception ex)
+ {
+ Log.Error(ex, "Could not load tex");
+ }
+ }
+
+ if (ImGui.Button("Load File"))
+ {
+ try
+ {
+ this.addedTextures.Add(texManager.GetTextureFromFile(new FileInfo(this.inputTexPath), this.keepAlive));
+ }
+ catch (Exception ex)
+ {
+ Log.Error(ex, "Could not load tex");
+ }
+ }
+
+ ImGui.Separator();
+ ImGui.InputFloat2("UV0", ref this.inputTexUv0);
+ ImGui.InputFloat2("UV1", ref this.inputTexUv1);
+ ImGui.InputFloat4("Tint", ref this.inputTintCol);
+ ImGui.InputFloat2("Scale", ref this.inputTexScale);
ImGuiHelpers.ScaledDummy(10);
- if (this.debugTex != null)
+ TextureWrap? toRemove = null;
+ for (var i = 0; i < this.addedTextures.Count; i++)
{
- ImGui.Image(this.debugTex.ImGuiHandle, this.inputTexScale, this.inputTexUv0, this.inputTexUv1, this.inputTintCol);
- ImGuiHelpers.ScaledDummy(5);
- Util.ShowObject(this.debugTex);
+ if (ImGui.CollapsingHeader($"Tex #{i}"))
+ {
+ var tex = this.addedTextures[i];
+
+ var scale = new Vector2(tex.Width, tex.Height);
+ if (this.inputTexScale != Vector2.Zero)
+ scale = this.inputTexScale;
+
+ ImGui.Image(tex.ImGuiHandle, scale, this.inputTexUv0, this.inputTexUv1, this.inputTintCol);
+
+ if (ImGui.Button($"X##{i}"))
+ toRemove = tex;
+ }
+ }
+
+ if (toRemove != null)
+ {
+ toRemove.Dispose();
+ this.addedTextures.Remove(toRemove);
}
}
}
diff --git a/Dalamud/Plugin/Services/IDataManager.cs b/Dalamud/Plugin/Services/IDataManager.cs
index e79a58fd5..ff9b40605 100644
--- a/Dalamud/Plugin/Services/IDataManager.cs
+++ b/Dalamud/Plugin/Services/IDataManager.cs
@@ -1,4 +1,5 @@
-using System.Collections.ObjectModel;
+using System;
+using System.Collections.ObjectModel;
using System.Diagnostics.CodeAnalysis;
using ImGuiScene;
@@ -92,6 +93,7 @@ public interface IDataManager
/// The icon ID.
/// Return high resolution version.
/// The containing the icon.
+ [Obsolete("Use ITextureProvider instead")]
public TexFile? GetIcon(uint iconId, bool highResolution = false);
///
@@ -101,6 +103,7 @@ public interface IDataManager
/// The icon ID.
/// Return high resolution version.
/// The containing the icon.
+ [Obsolete("Use ITextureProvider instead")]
public TexFile? GetIcon(ClientLanguage iconLanguage, uint iconId, bool highResolution = false);
///
@@ -110,6 +113,7 @@ public interface IDataManager
/// The icon ID.
/// Return high resolution version.
/// The containing the icon.
+ [Obsolete("Use ITextureProvider instead")]
public TexFile? GetIcon(string? type, uint iconId, bool highResolution = false);
///
@@ -118,6 +122,7 @@ public interface IDataManager
/// The icon ID.
/// Return the high resolution version.
/// The containing the icon.
+ [Obsolete("Use ITextureProvider instead")]
public TextureWrap? GetImGuiTextureIcon(uint iconId, bool highResolution = false);
///
@@ -126,6 +131,7 @@ public interface IDataManager
/// A value indicating whether the icon should be HQ.
/// The icon ID.
/// The containing the icon.
+ [Obsolete("Use ITextureProvider instead")]
public TexFile? GetIcon(bool isHq, uint iconId);
///
@@ -133,6 +139,7 @@ public interface IDataManager
///
/// The icon ID.
/// The containing the icon.
+ [Obsolete("Use ITextureProvider instead")]
public TexFile? GetHqIcon(uint iconId);
///
@@ -140,6 +147,7 @@ public interface IDataManager
///
/// The Lumina .
/// A that can be used to draw the texture.
+ [Obsolete("Use ITextureProvider instead")]
[return: NotNullIfNotNull(nameof(tex))]
public TextureWrap? GetImGuiTexture(TexFile? tex);
@@ -148,6 +156,7 @@ public interface IDataManager
///
/// The internal path to the texture.
/// A that can be used to draw the texture.
+ [Obsolete("Use ITextureProvider instead")]
public TextureWrap? GetImGuiTexture(string path);
///
@@ -156,6 +165,7 @@ public interface IDataManager
/// A value indicating whether the icon should be HQ.
/// The icon ID.
/// The containing the icon.
+ [Obsolete("Use ITextureProvider instead")]
public TextureWrap? GetImGuiTextureIcon(bool isHq, uint iconId);
///
@@ -164,6 +174,7 @@ public interface IDataManager
/// The requested language.
/// The icon ID.
/// The containing the icon.
+ [Obsolete("Use ITextureProvider instead")]
public TextureWrap? GetImGuiTextureIcon(ClientLanguage iconLanguage, uint iconId);
///
@@ -172,6 +183,7 @@ public interface IDataManager
/// The type of the icon (e.g. 'hq' to get the HQ variant of an item icon).
/// The icon ID.
/// The containing the icon.
+ [Obsolete("Use ITextureProvider instead")]
public TextureWrap? GetImGuiTextureIcon(string type, uint iconId);
///
@@ -179,5 +191,6 @@ public interface IDataManager
///
/// The icon ID.
/// The containing the icon.
+ [Obsolete("Use ITextureProvider instead")]
public TextureWrap? GetImGuiTextureHqIcon(uint iconId);
}
diff --git a/Dalamud/Plugin/Services/ITextureProvider.cs b/Dalamud/Plugin/Services/ITextureProvider.cs
new file mode 100644
index 000000000..091b2ed67
--- /dev/null
+++ b/Dalamud/Plugin/Services/ITextureProvider.cs
@@ -0,0 +1,96 @@
+using System;
+using System.IO;
+
+using Dalamud.Interface.Internal;
+using Lumina.Data.Files;
+
+namespace Dalamud.Plugin.Services;
+
+///
+/// Service that grants you access to textures you may render via ImGui.
+///
+public interface ITextureProvider
+{
+ ///
+ /// Flags describing the icon you wish to receive.
+ ///
+ [Flags]
+ public enum IconFlags
+ {
+ ///
+ /// Low-resolution, standard quality icon.
+ ///
+ None = 0,
+
+ ///
+ /// If this icon is an item icon, and it has a high-quality variant, receive the high-quality version.
+ /// Null if the item does not have a high-quality variant.
+ ///
+ ItemHighQuality = 1 << 0,
+
+ ///
+ /// Get the hi-resolution version of the icon, if it exists.
+ ///
+ HiRes = 1 << 1,
+ }
+
+ ///
+ /// Get a texture handle for a specific icon.
+ ///
+ /// The ID of the icon to load.
+ /// Options to be considered when loading the icon.
+ ///
+ /// The language to be considered when loading the icon, if the icon has versions for multiple languages.
+ /// If null, default to the game's current language.
+ ///
+ ///
+ /// Prevent Dalamud from automatically unloading this icon to save memory. Usually does not need to be set.
+ ///
+ ///
+ /// Null, if the icon does not exist in the specified configuration, or a texture wrap that can be used
+ /// to render the icon.
+ ///
+ public IDalamudTextureWrap? GetIcon(uint iconId, IconFlags flags = IconFlags.HiRes, ClientLanguage? language = null, bool keepAlive = false);
+
+ ///
+ /// Get a path for a specific icon's .tex file.
+ ///
+ /// The ID of the icon to look up.
+ /// Options to be considered when loading the icon.
+ ///
+ /// The language to be considered when loading the icon, if the icon has versions for multiple languages.
+ /// If null, default to the game's current language.
+ ///
+ ///
+ /// Null, if the icon does not exist in the specified configuration, or the path to the texture's .tex file,
+ /// which can be loaded via IDataManager.
+ ///
+ public string? GetIconPath(uint iconId, IconFlags flags = IconFlags.HiRes, ClientLanguage? language = null);
+
+ ///
+ /// Get a texture handle for the texture at the specified path.
+ /// You may only specify paths in the game's VFS.
+ ///
+ /// The path to the texture in the game's VFS.
+ /// Prevent Dalamud from automatically unloading this texture to save memory. Usually does not need to be set.
+ /// Null, if the icon does not exist, or a texture wrap that can be used to render the texture.
+ public IDalamudTextureWrap? GetTextureFromGame(string path, bool keepAlive = false);
+
+ ///
+ /// Get a texture handle for the image or texture, specified by the passed FileInfo.
+ /// You may only specify paths on the native file system.
+ ///
+ /// This API can load .png and .tex files.
+ ///
+ /// The FileInfo describing the image or texture file.
+ /// Prevent Dalamud from automatically unloading this texture to save memory. Usually does not need to be set.
+ /// Null, if the file does not exist, or a texture wrap that can be used to render the texture.
+ public IDalamudTextureWrap? GetTextureFromFile(FileInfo file, bool keepAlive = false);
+
+ ///
+ /// Get a texture handle for the specified Lumina TexFile.
+ ///
+ /// The texture to obtain a handle to.
+ /// A texture wrap that can be used to render the texture.
+ public IDalamudTextureWrap GetTexture(TexFile file);
+}
diff --git a/Dalamud/Plugin/Services/ITextureSubstitutionProvider.cs b/Dalamud/Plugin/Services/ITextureSubstitutionProvider.cs
new file mode 100644
index 000000000..90be71adb
--- /dev/null
+++ b/Dalamud/Plugin/Services/ITextureSubstitutionProvider.cs
@@ -0,0 +1,20 @@
+namespace Dalamud.Plugin.Services;
+
+///
+/// Service that grants you the ability to replace texture data that is to be loaded by Dalamud.
+///
+public interface ITextureSubstitutionProvider
+{
+ ///
+ /// Delegate describing a function that may be used to intercept and replace texture data.
+ /// The path assigned may point to another texture inside the game's dats, or a .tex file or image on the disk.
+ ///
+ /// The path to the texture that is to be loaded.
+ /// The path that should be loaded instead.
+ public delegate void TextureDataInterceptorDelegate(string path, ref string? replacementPath);
+
+ ///
+ /// Event that will be called once Dalamud wants to load texture data.
+ ///
+ public event TextureDataInterceptorDelegate? InterceptTexDataLoad;
+}