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/TextureManager.cs b/Dalamud/Interface/Internal/TextureManager.cs new file mode 100644 index 000000000..05b10263b --- /dev/null +++ b/Dalamud/Interface/Internal/TextureManager.cs @@ -0,0 +1,458 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Reflection; + +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; +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 DalamudStartInfo startInfo; + + private readonly Dictionary activeTextures = new(); + + /// + /// Initializes a new instance of the class. + /// + /// Framework instance. + /// DataManager instance. + /// DalamudStartInfo instance. + [ServiceManager.ServiceConstructor] + public TextureManager(Framework framework, DataManager dataManager, DalamudStartInfo startInfo) + { + this.framework = framework; + this.dataManager = dataManager; + this.startInfo = startInfo; + + this.framework.Update += this.FrameworkOnUpdate; + } + + /// + 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.ItemHighQuality, ClientLanguage? language = null, bool keepAlive = false) + { + 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 this.CreateWrap(path, keepAlive); + + 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 this.CreateWrap(path, keepAlive); + + if (hiRes) + { + // 3. Regular icon, with language, no hi-res + path = FormatIconPath( + iconId, + languageFolder, + false); + if (this.dataManager.FileExists(path)) + return this.CreateWrap(path, keepAlive); + } + + // 4. Regular icon, without language, hi-res + path = FormatIconPath( + iconId, + null, + hiRes); + if (this.dataManager.FileExists(path)) + return this.CreateWrap(path, keepAlive); + + // 4. Regular icon, without language, no hi-res + if (hiRes) + { + path = FormatIconPath( + iconId, + null, + false); + if (this.dataManager.FileExists(path)) + return this.CreateWrap(path, keepAlive); + } + + 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? GetTextureFromGamePath(string path, bool keepAlive) + { + return !this.dataManager.FileExists(path) ? null : this.CreateWrap(path, keepAlive); + } + + /// + public void 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. + /// Info object storing texture metadata. + internal TextureInfo GetInfo(string path, bool refresh = true) + { + 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, this might not be an issue", path); + } + } + + if (refresh && info.KeepAliveCount == 0) + info.LastAccess = DateTime.Now; + + if (info is { Wrap: not null }) + return info; + + if (refresh) + { + byte[]? interceptData = null; + this.InterceptTexDataLoad?.Invoke(path, ref interceptData); + + // TODO: Do we also want to support loading from actual fs here? Doesn't seem to be a big deal, collect interest + + TexFile? file; + if (interceptData != null) + { + // TODO: upstream to lumina + file = Activator.CreateInstance(); + var type = typeof(TexFile); + type.GetProperty("Data", BindingFlags.NonPublic | BindingFlags.Instance)!.GetSetMethod()! + .Invoke(file, new object[] { interceptData }); + type.GetProperty("Reader", BindingFlags.NonPublic | BindingFlags.Instance)!.GetSetMethod()! + .Invoke(file, new object[] { new LuminaBinaryReader(file.Data) }); + file.LoadFile(); + } + else + { + file = this.dataManager.GetFile(path); + } + + var wrap = this.dataManager.GetImGuiTexture(file); + 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, keepAlive); + info.RefCount++; + + if (keepAlive) + info.KeepAliveCount++; + + return new TextureManagerTextureWrap(path, 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.Now - 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); + } + } + } + + /// + /// 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; } + } +} + +/// +/// 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 DataManager dataManager; + private readonly TextureManager textureManager; + + private readonly List trackedTextures = new(); + + /// + /// Initializes a new instance of the class. + /// + /// DataManager instance. + /// TextureManager instance. + public TextureManagerPluginScoped(DataManager dataManager, TextureManager textureManager) + { + this.dataManager = dataManager; + 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 IDalamudTextureWrap? GetTextureFromGamePath(string path, bool keepAlive = false) + { + var wrap = this.textureManager.GetTextureFromGamePath(path, keepAlive); + if (wrap == null) + return null; + + this.trackedTextures.Add(wrap); + return wrap; + } + + /// + public IDalamudTextureWrap GetTexture(TexFile file) + { + return this.dataManager.GetImGuiTexture(file) as DalamudTextureWrap ?? throw new ArgumentException("Could not load texture"); + } + + /// + 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; + + private int? width; + private int? height; + + /// + /// Initializes a new instance of the class. + /// + /// The path to the texture. + /// Keep alive or not. + /// Manager that we obtained this from. + internal TextureManagerTextureWrap(string path, bool keepAlive, TextureManager manager) + { + this.path = path; + this.keepAlive = keepAlive; + this.manager = manager; + } + + /// + public IntPtr ImGuiHandle => this.manager.GetInfo(this.path).Wrap!.ImGuiHandle; + + /// + public int Width => this.width ??= this.manager.GetInfo(this.path).Wrap!.Width; + + /// + public int Height => this.height ??= this.manager.GetInfo(this.path).Wrap!.Height; + + /// + /// 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..d5335d170 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs @@ -1,7 +1,9 @@ using System; +using System.Collections.Generic; using System.Numerics; using Dalamud.Data; +using Dalamud.Plugin.Services; using Dalamud.Utility; using ImGuiNET; using ImGuiScene; @@ -14,13 +16,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 +43,75 @@ 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.GetTextureFromGamePath(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 fa8c5bf43..8e36905f9 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 ImGuiScene; using Lumina; @@ -91,6 +92,7 @@ public interface IDataManager /// The icon ID. /// Return high resolution version. /// The containing the icon. + [Obsolete("Use ITextureManager instead")] public TexFile? GetIcon(uint iconId, bool highResolution = false); /// @@ -100,6 +102,7 @@ public interface IDataManager /// The icon ID. /// Return high resolution version. /// The containing the icon. + [Obsolete("Use ITextureManager instead")] public TexFile? GetIcon(ClientLanguage iconLanguage, uint iconId, bool highResolution = false); /// @@ -109,6 +112,7 @@ public interface IDataManager /// The icon ID. /// Return high resolution version. /// The containing the icon. + [Obsolete("Use ITextureManager instead")] public TexFile? GetIcon(string? type, uint iconId, bool highResolution = false); /// @@ -117,6 +121,7 @@ public interface IDataManager /// The icon ID. /// Return the high resolution version. /// The containing the icon. + [Obsolete("Use ITextureManager instead")] public TextureWrap? GetImGuiTextureIcon(uint iconId, bool highResolution = false); /// @@ -125,6 +130,7 @@ public interface IDataManager /// A value indicating whether the icon should be HQ. /// The icon ID. /// The containing the icon. + [Obsolete("Use ITextureManager instead")] public TexFile? GetIcon(bool isHq, uint iconId); /// @@ -132,6 +138,7 @@ public interface IDataManager /// /// The icon ID. /// The containing the icon. + [Obsolete("Use ITextureManager instead")] public TexFile? GetHqIcon(uint iconId); /// @@ -139,6 +146,7 @@ public interface IDataManager /// /// The Lumina . /// A that can be used to draw the texture. + [Obsolete("Use ITextureManager instead")] public TextureWrap? GetImGuiTexture(TexFile? tex); /// @@ -146,6 +154,7 @@ public interface IDataManager /// /// The internal path to the texture. /// A that can be used to draw the texture. + [Obsolete("Use ITextureManager instead")] public TextureWrap? GetImGuiTexture(string path); /// @@ -154,6 +163,7 @@ public interface IDataManager /// A value indicating whether the icon should be HQ. /// The icon ID. /// The containing the icon. + [Obsolete("Use ITextureManager instead")] public TextureWrap? GetImGuiTextureIcon(bool isHq, uint iconId); /// @@ -162,6 +172,7 @@ public interface IDataManager /// The requested language. /// The icon ID. /// The containing the icon. + [Obsolete("Use ITextureManager instead")] public TextureWrap? GetImGuiTextureIcon(ClientLanguage iconLanguage, uint iconId); /// @@ -170,6 +181,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 ITextureManager instead")] public TextureWrap? GetImGuiTextureIcon(string type, uint iconId); /// @@ -177,5 +189,6 @@ public interface IDataManager /// /// The icon ID. /// The containing the icon. + [Obsolete("Use ITextureManager 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..56ed310e8 --- /dev/null +++ b/Dalamud/Plugin/Services/ITextureProvider.cs @@ -0,0 +1,69 @@ +using System; + +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 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? GetTextureFromGamePath(string path, 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..fcc27f8e6 --- /dev/null +++ b/Dalamud/Plugin/Services/ITextureSubstitutionProvider.cs @@ -0,0 +1,21 @@ +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 to the texture that is to be loaded. + /// The texture data. Null by default, assign something if you wish to replace the data from the game dats. + public delegate void TextureDataInterceptorDelegate(string path, ref byte[]? data); + + /// + /// Event that will be called once Dalamud wants to load texture data. + /// If you have data that should replace the data from the game dats, assign it to the + /// data argument. + /// + public event TextureDataInterceptorDelegate? InterceptTexDataLoad; +}