Merge pull request #1335 from goaaats/tex_rework

This commit is contained in:
goat 2023-08-03 20:26:47 +02:00 committed by GitHub
commit 36bfad7582
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 837 additions and 25 deletions

View file

@ -188,15 +188,17 @@ public sealed class DataManager : IDisposable, IServiceType, IDataManager
/// </summary>
/// <param name="iconId">The icon ID.</param>
/// <returns>The <see cref="TexFile"/> containing the icon.</returns>
/// 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);
/// <inheritdoc/>
[Obsolete("Use ITextureProvider instead")]
public TexFile? GetIcon(uint iconId, bool highResolution)
=> this.GetIcon(this.Language, iconId, highResolution);
/// <inheritdoc/>
[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
/// <param name="iconLanguage">The requested language.</param>
/// <param name="iconId">The icon ID.</param>
/// <returns>The <see cref="TexFile"/> containing the icon.</returns>
/// 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);
/// <inheritdoc/>
[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
/// <param name="type">The type of the icon (e.g. 'hq' to get the HQ variant of an item icon).</param>
/// <param name="iconId">The icon ID.</param>
/// <returns>The <see cref="TexFile"/> containing the icon.</returns>
/// 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);
/// <inheritdoc/>
[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
}
/// <inheritdoc/>
[Obsolete("Use ITextureProvider instead")]
public TexFile? GetHqIcon(uint iconId)
=> this.GetIcon(true, iconId);
/// <inheritdoc/>
[Obsolete("Use ITextureProvider instead")]
[return: NotNullIfNotNull(nameof(tex))]
public TextureWrap? GetImGuiTexture(TexFile? tex)
{
@ -290,6 +296,7 @@ public sealed class DataManager : IDisposable, IServiceType, IDataManager
}
/// <inheritdoc/>
[Obsolete("Use ITextureProvider instead")]
public TextureWrap? GetImGuiTexture(string path)
=> this.GetImGuiTexture(this.GetFile<TexFile>(path));
@ -299,26 +306,32 @@ public sealed class DataManager : IDisposable, IServiceType, IDataManager
/// <param name="iconId">The icon ID.</param>
/// <returns>The <see cref="TextureWrap"/> containing the icon.</returns>
/// 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));
/// <inheritdoc/>
[Obsolete("Use ITextureProvider instead")]
public TextureWrap? GetImGuiTextureIcon(uint iconId, bool highResolution)
=> this.GetImGuiTexture(this.GetIcon(iconId, highResolution));
/// <inheritdoc/>
[Obsolete("Use ITextureProvider instead")]
public TextureWrap? GetImGuiTextureIcon(bool isHq, uint iconId)
=> this.GetImGuiTexture(this.GetIcon(isHq, iconId));
/// <inheritdoc/>
[Obsolete("Use ITextureProvider instead")]
public TextureWrap? GetImGuiTextureIcon(ClientLanguage iconLanguage, uint iconId)
=> this.GetImGuiTexture(this.GetIcon(iconLanguage, iconId));
/// <inheritdoc/>
[Obsolete("Use ITextureProvider instead")]
public TextureWrap? GetImGuiTextureIcon(string type, uint iconId)
=> this.GetImGuiTexture(this.GetIcon(type, iconId));
/// <inheritdoc/>
[Obsolete("Use ITextureProvider instead")]
public TextureWrap? GetImGuiTextureHqIcon(uint iconId)
=> this.GetImGuiTexture(this.GetHqIcon(iconId));

View file

@ -4,11 +4,19 @@ using ImGuiScene;
namespace Dalamud.Interface.Internal;
/// <summary>
/// Base TextureWrap interface for all Dalamud-owned texture wraps.
/// Used to avoid referencing ImGuiScene.
/// </summary>
public interface IDalamudTextureWrap : TextureWrap
{
}
/// <summary>
/// Safety harness for ImGuiScene textures that will defer destruction until
/// the end of the frame.
/// </summary>
public class DalamudTextureWrap : TextureWrap
public class DalamudTextureWrap : IDalamudTextureWrap
{
private readonly TextureWrap wrappedWrap;

View file

@ -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)]

View file

@ -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;
/// <summary>
/// Service responsible for loading and disposing ImGui texture wraps.
/// </summary>
[PluginInterface]
[InterfaceVersion("1.0")]
[ServiceManager.BlockingEarlyLoadedService]
#pragma warning disable SA1015
[ResolveVia<ITextureSubstitutionProvider>]
#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<string, TextureInfo> activeTextures = new();
private TextureWrap? fallbackTextureWrap;
/// <summary>
/// Initializes a new instance of the <see cref="TextureManager"/> class.
/// </summary>
/// <param name="framework">Framework instance.</param>
/// <param name="dataManager">DataManager instance.</param>
/// <param name="im">InterfaceManager instance.</param>
/// <param name="startInfo">DalamudStartInfo instance.</param>
[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<InterfaceManager.InterfaceManagerWithScene>.GetAsync().ContinueWith(_ => this.CreateFallbackTexture());
}
/// <inheritdoc/>
public event ITextureSubstitutionProvider.TextureDataInterceptorDelegate? InterceptTexDataLoad;
/// <summary>
/// Get a texture handle for a specific icon.
/// </summary>
/// <param name="iconId">The ID of the icon to load.</param>
/// <param name="flags">Options to be considered when loading the icon.</param>
/// <param name="language">
/// 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.
/// </param>
/// <param name="keepAlive">
/// Prevent Dalamud from automatically unloading this icon to save memory. Usually does not need to be set.
/// </param>
/// <returns>
/// Null, if the icon does not exist in the specified configuration, or a texture wrap that can be used
/// to render the icon.
/// </returns>
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);
}
/// <summary>
/// Get a path for a specific icon's .tex file.
/// </summary>
/// <param name="iconId">The ID of the icon to look up.</param>
/// <param name="flags">Options to be considered when loading the icon.</param>
/// <param name="language">
/// 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.
/// </param>
/// <returns>
/// 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.
/// </returns>
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;
}
/// <summary>
/// Get a texture handle for the texture at the specified path.
/// You may only specify paths in the game's VFS.
/// </summary>
/// <param name="path">The path to the texture in the game's VFS.</param>
/// <param name="keepAlive">Prevent Dalamud from automatically unloading this texture to save memory. Usually does not need to be set.</param>
/// <returns>Null, if the icon does not exist, or a texture wrap that can be used to render the texture.</returns>
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);
}
/// <summary>
/// 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.
/// </summary>
/// <param name="file">The FileInfo describing the image or texture file.</param>
/// <param name="keepAlive">Prevent Dalamud from automatically unloading this texture to save memory. Usually does not need to be set.</param>
/// <returns>Null, if the file does not exist, or a texture wrap that can be used to render the texture.</returns>
public TextureManagerTextureWrap? GetTextureFromFile(FileInfo file, bool keepAlive = false)
{
ArgumentNullException.ThrowIfNull(file);
return !file.Exists ? null : this.CreateWrap(file.FullName, keepAlive);
}
/// <summary>
/// Get a texture handle for the specified Lumina TexFile.
/// </summary>
/// <param name="file">The texture to obtain a handle to.</param>
/// <returns>A texture wrap that can be used to render the texture.</returns>
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
}
/// <inheritdoc/>
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();
}
/// <summary>
/// Get texture info.
/// </summary>
/// <param name="path">Path to the texture.</param>
/// <param name="refresh">Whether or not the texture should be reloaded if it was unloaded.</param>
/// <param name="rethrow">
/// 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.
/// </param>
/// <returns>Info object storing texture metadata.</returns>
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<TexFile>(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<TexFile>(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;
}
/// <summary>
/// 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.
/// </summary>
/// <param name="path">The path to the texture.</param>
/// <param name="keepAlive">Whether or not this handle was created in keep-alive mode.</param>
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<string>();
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");
}
/// <summary>
/// Internal representation of a managed texture.
/// </summary>
internal class TextureInfo
{
/// <summary>
/// Gets or sets the actual texture wrap. May be unpopulated.
/// </summary>
public TextureWrap? Wrap { get; set; }
/// <summary>
/// Gets or sets the time the texture was last accessed.
/// </summary>
public DateTime LastAccess { get; set; }
/// <summary>
/// Gets or sets the number of active holders of this texture.
/// </summary>
public uint RefCount { get; set; }
/// <summary>
/// Gets or sets the number of active holders that want this texture to stay alive forever.
/// </summary>
public uint KeepAliveCount { get; set; }
/// <summary>
/// Gets or sets the extents of the texture.
/// </summary>
public Vector2 Extents { get; set; }
}
}
/// <summary>
/// Plugin-scoped version of a texture manager.
/// </summary>
[PluginInterface]
[InterfaceVersion("1.0")]
[ServiceManager.ScopedService]
#pragma warning disable SA1015
[ResolveVia<ITextureProvider>]
#pragma warning restore SA1015
internal class TextureManagerPluginScoped : ITextureProvider, IServiceType, IDisposable
{
private readonly TextureManager textureManager;
private readonly List<TextureManagerTextureWrap> trackedTextures = new();
/// <summary>
/// Initializes a new instance of the <see cref="TextureManagerPluginScoped"/> class.
/// </summary>
/// <param name="textureManager">TextureManager instance.</param>
public TextureManagerPluginScoped(TextureManager textureManager)
{
this.textureManager = textureManager;
}
/// <inheritdoc/>
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;
}
/// <inheritdoc/>
public string? GetIconPath(uint iconId, ITextureProvider.IconFlags flags = ITextureProvider.IconFlags.HiRes, ClientLanguage? language = null)
=> this.textureManager.GetIconPath(iconId, flags, language);
/// <inheritdoc/>
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;
}
/// <inheritdoc/>
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;
}
/// <inheritdoc/>
public IDalamudTextureWrap? GetTexture(TexFile file)
=> this.textureManager.GetTexture(file);
/// <inheritdoc/>
public void Dispose()
{
// Dispose all leaked textures
foreach (var textureWrap in this.trackedTextures.Where(x => !x.IsDisposed))
{
textureWrap.Dispose();
}
}
}
/// <summary>
/// Wrap.
/// </summary>
internal class TextureManagerTextureWrap : IDalamudTextureWrap
{
private readonly TextureManager manager;
private readonly string path;
private readonly bool keepAlive;
/// <summary>
/// Initializes a new instance of the <see cref="TextureManagerTextureWrap"/> class.
/// </summary>
/// <param name="path">The path to the texture.</param>
/// <param name="extents">The extents of the texture.</param>
/// <param name="keepAlive">Keep alive or not.</param>
/// <param name="manager">Manager that we obtained this from.</param>
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;
}
/// <inheritdoc/>
public IntPtr ImGuiHandle => this.manager.GetInfo(this.path).Wrap!.ImGuiHandle;
/// <inheritdoc/>
public int Width { get; private set; }
/// <inheritdoc/>
public int Height { get; private set; }
/// <summary>
/// Gets a value indicating whether or not this wrap has already been disposed.
/// If true, the handle may be invalid.
/// </summary>
internal bool IsDisposed { get; private set; }
/// <inheritdoc/>
public void Dispose()
{
lock (this)
{
if (!this.IsDisposed)
this.manager.NotifyTextureDisposed(this.path, this.keepAlive);
this.IsDisposed = true;
}
}
}

View file

@ -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;
/// </summary>
internal class TexWidget : IDataWindowWidget
{
private readonly List<TextureWrap> 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;
/// <inheritdoc/>
public DataKind DataKind { get; init; } = DataKind.Tex;
@ -36,34 +42,87 @@ internal class TexWidget : IDataWindowWidget
/// <inheritdoc/>
public void Draw()
{
var dataManager = Service<DataManager>.Get();
var texManager = Service<TextureManager>.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);
}
}
}

View file

@ -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
/// <param name="iconId">The icon ID.</param>
/// <param name="highResolution">Return high resolution version.</param>
/// <returns>The <see cref="TexFile"/> containing the icon.</returns>
[Obsolete("Use ITextureProvider instead")]
public TexFile? GetIcon(uint iconId, bool highResolution = false);
/// <summary>
@ -101,6 +103,7 @@ public interface IDataManager
/// <param name="iconId">The icon ID.</param>
/// <param name="highResolution">Return high resolution version.</param>
/// <returns>The <see cref="TexFile"/> containing the icon.</returns>
[Obsolete("Use ITextureProvider instead")]
public TexFile? GetIcon(ClientLanguage iconLanguage, uint iconId, bool highResolution = false);
/// <summary>
@ -110,6 +113,7 @@ public interface IDataManager
/// <param name="iconId">The icon ID.</param>
/// <param name="highResolution">Return high resolution version.</param>
/// <returns>The <see cref="TexFile"/> containing the icon.</returns>
[Obsolete("Use ITextureProvider instead")]
public TexFile? GetIcon(string? type, uint iconId, bool highResolution = false);
/// <summary>
@ -118,6 +122,7 @@ public interface IDataManager
/// <param name="iconId">The icon ID.</param>
/// <param name="highResolution">Return the high resolution version.</param>
/// <returns>The <see cref="TextureWrap"/> containing the icon.</returns>
[Obsolete("Use ITextureProvider instead")]
public TextureWrap? GetImGuiTextureIcon(uint iconId, bool highResolution = false);
/// <summary>
@ -126,6 +131,7 @@ public interface IDataManager
/// <param name="isHq">A value indicating whether the icon should be HQ.</param>
/// <param name="iconId">The icon ID.</param>
/// <returns>The <see cref="TexFile"/> containing the icon.</returns>
[Obsolete("Use ITextureProvider instead")]
public TexFile? GetIcon(bool isHq, uint iconId);
/// <summary>
@ -133,6 +139,7 @@ public interface IDataManager
/// </summary>
/// <param name="iconId">The icon ID.</param>
/// <returns>The <see cref="TexFile"/> containing the icon.</returns>
[Obsolete("Use ITextureProvider instead")]
public TexFile? GetHqIcon(uint iconId);
/// <summary>
@ -140,6 +147,7 @@ public interface IDataManager
/// </summary>
/// <param name="tex">The Lumina <see cref="TexFile"/>.</param>
/// <returns>A <see cref="TextureWrap"/> that can be used to draw the texture.</returns>
[Obsolete("Use ITextureProvider instead")]
[return: NotNullIfNotNull(nameof(tex))]
public TextureWrap? GetImGuiTexture(TexFile? tex);
@ -148,6 +156,7 @@ public interface IDataManager
/// </summary>
/// <param name="path">The internal path to the texture.</param>
/// <returns>A <see cref="TextureWrap"/> that can be used to draw the texture.</returns>
[Obsolete("Use ITextureProvider instead")]
public TextureWrap? GetImGuiTexture(string path);
/// <summary>
@ -156,6 +165,7 @@ public interface IDataManager
/// <param name="isHq">A value indicating whether the icon should be HQ.</param>
/// <param name="iconId">The icon ID.</param>
/// <returns>The <see cref="TextureWrap"/> containing the icon.</returns>
[Obsolete("Use ITextureProvider instead")]
public TextureWrap? GetImGuiTextureIcon(bool isHq, uint iconId);
/// <summary>
@ -164,6 +174,7 @@ public interface IDataManager
/// <param name="iconLanguage">The requested language.</param>
/// <param name="iconId">The icon ID.</param>
/// <returns>The <see cref="TextureWrap"/> containing the icon.</returns>
[Obsolete("Use ITextureProvider instead")]
public TextureWrap? GetImGuiTextureIcon(ClientLanguage iconLanguage, uint iconId);
/// <summary>
@ -172,6 +183,7 @@ public interface IDataManager
/// <param name="type">The type of the icon (e.g. 'hq' to get the HQ variant of an item icon).</param>
/// <param name="iconId">The icon ID.</param>
/// <returns>The <see cref="TextureWrap"/> containing the icon.</returns>
[Obsolete("Use ITextureProvider instead")]
public TextureWrap? GetImGuiTextureIcon(string type, uint iconId);
/// <summary>
@ -179,5 +191,6 @@ public interface IDataManager
/// </summary>
/// <param name="iconId">The icon ID.</param>
/// <returns>The <see cref="TextureWrap"/> containing the icon.</returns>
[Obsolete("Use ITextureProvider instead")]
public TextureWrap? GetImGuiTextureHqIcon(uint iconId);
}

View file

@ -0,0 +1,96 @@
using System;
using System.IO;
using Dalamud.Interface.Internal;
using Lumina.Data.Files;
namespace Dalamud.Plugin.Services;
/// <summary>
/// Service that grants you access to textures you may render via ImGui.
/// </summary>
public interface ITextureProvider
{
/// <summary>
/// Flags describing the icon you wish to receive.
/// </summary>
[Flags]
public enum IconFlags
{
/// <summary>
/// Low-resolution, standard quality icon.
/// </summary>
None = 0,
/// <summary>
/// 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.
/// </summary>
ItemHighQuality = 1 << 0,
/// <summary>
/// Get the hi-resolution version of the icon, if it exists.
/// </summary>
HiRes = 1 << 1,
}
/// <summary>
/// Get a texture handle for a specific icon.
/// </summary>
/// <param name="iconId">The ID of the icon to load.</param>
/// <param name="flags">Options to be considered when loading the icon.</param>
/// <param name="language">
/// 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.
/// </param>
/// <param name="keepAlive">
/// Prevent Dalamud from automatically unloading this icon to save memory. Usually does not need to be set.
/// </param>
/// <returns>
/// Null, if the icon does not exist in the specified configuration, or a texture wrap that can be used
/// to render the icon.
/// </returns>
public IDalamudTextureWrap? GetIcon(uint iconId, IconFlags flags = IconFlags.HiRes, ClientLanguage? language = null, bool keepAlive = false);
/// <summary>
/// Get a path for a specific icon's .tex file.
/// </summary>
/// <param name="iconId">The ID of the icon to look up.</param>
/// <param name="flags">Options to be considered when loading the icon.</param>
/// <param name="language">
/// 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.
/// </param>
/// <returns>
/// 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.
/// </returns>
public string? GetIconPath(uint iconId, IconFlags flags = IconFlags.HiRes, ClientLanguage? language = null);
/// <summary>
/// Get a texture handle for the texture at the specified path.
/// You may only specify paths in the game's VFS.
/// </summary>
/// <param name="path">The path to the texture in the game's VFS.</param>
/// <param name="keepAlive">Prevent Dalamud from automatically unloading this texture to save memory. Usually does not need to be set.</param>
/// <returns>Null, if the icon does not exist, or a texture wrap that can be used to render the texture.</returns>
public IDalamudTextureWrap? GetTextureFromGame(string path, bool keepAlive = false);
/// <summary>
/// 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.
/// </summary>
/// <param name="file">The FileInfo describing the image or texture file.</param>
/// <param name="keepAlive">Prevent Dalamud from automatically unloading this texture to save memory. Usually does not need to be set.</param>
/// <returns>Null, if the file does not exist, or a texture wrap that can be used to render the texture.</returns>
public IDalamudTextureWrap? GetTextureFromFile(FileInfo file, bool keepAlive = false);
/// <summary>
/// Get a texture handle for the specified Lumina TexFile.
/// </summary>
/// <param name="file">The texture to obtain a handle to.</param>
/// <returns>A texture wrap that can be used to render the texture.</returns>
public IDalamudTextureWrap GetTexture(TexFile file);
}

View file

@ -0,0 +1,20 @@
namespace Dalamud.Plugin.Services;
/// <summary>
/// Service that grants you the ability to replace texture data that is to be loaded by Dalamud.
/// </summary>
public interface ITextureSubstitutionProvider
{
/// <summary>
/// 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.
/// </summary>
/// <param name="path">The path to the texture that is to be loaded.</param>
/// <param name="replacementPath">The path that should be loaded instead.</param>
public delegate void TextureDataInterceptorDelegate(string path, ref string? replacementPath);
/// <summary>
/// Event that will be called once Dalamud wants to load texture data.
/// </summary>
public event TextureDataInterceptorDelegate? InterceptTexDataLoad;
}