diff --git a/OtterGui b/OtterGui index 8d61845c..9dad9558 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 8d61845cd900fc0a3b58d475c43303b13c1165f4 +Subproject commit 9dad955808831a5d154d778d1123acbe648c42ac diff --git a/Penumbra.Api b/Penumbra.Api index 983c98f7..623e802b 160000 --- a/Penumbra.Api +++ b/Penumbra.Api @@ -1 +1 @@ -Subproject commit 983c98f74e7cd052b21f6ca35ef0ceaa9b388964 +Subproject commit 623e802bbc18496aab4030b444154a5b015093c2 diff --git a/Penumbra/Api/IpcTester.cs b/Penumbra/Api/IpcTester.cs index d1124847..fea91b0e 100644 --- a/Penumbra/Api/IpcTester.cs +++ b/Penumbra/Api/IpcTester.cs @@ -9,6 +9,8 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Numerics; +using System.Reflection.Metadata.Ecma335; +using System.Threading.Tasks; using Dalamud.Utility; using Penumbra.Api.Enums; using Penumbra.Api.Helpers; @@ -19,7 +21,6 @@ using Penumbra.Mods.Manager; using Penumbra.Services; using Penumbra.UI; using Penumbra.Collections.Manager; -using Penumbra.Util; namespace Penumbra.Api; @@ -38,6 +39,7 @@ public class IpcTester : IDisposable private readonly Meta _meta; private readonly Mods _mods; private readonly ModSettings _modSettings; + private readonly Editing _editing; private readonly Temporary _temporary; public IpcTester(Configuration config, DalamudServices dalamud, PenumbraIpcProviders ipcProviders, ModManager modManager, @@ -54,6 +56,7 @@ public class IpcTester : IDisposable _meta = new Meta(dalamud.PluginInterface); _mods = new Mods(dalamud.PluginInterface); _modSettings = new ModSettings(dalamud.PluginInterface); + _editing = new Editing(dalamud.PluginInterface); _temporary = new Temporary(dalamud.PluginInterface, modManager, collections, tempMods, tempCollections, saveService, config); UnsubscribeEvents(); } @@ -74,6 +77,7 @@ public class IpcTester : IDisposable _meta.Draw(); _mods.Draw(); _modSettings.Draw(); + _editing.Draw(); _temporary.Draw(); _temporary.DrawCollections(); _temporary.DrawMods(); @@ -402,9 +406,9 @@ public class IpcTester : IDisposable private string _lastRedrawnString = "None"; public Redrawing(DalamudServices dalamud) - { + { _dalamud = dalamud; - Redrawn = Ipc.GameObjectRedrawn.Subscriber(_dalamud.PluginInterface, SetLastRedrawn); + Redrawn = Ipc.GameObjectRedrawn.Subscriber(_dalamud.PluginInterface, SetLastRedrawn); } public void Draw() @@ -1149,6 +1153,72 @@ public class IpcTester : IDisposable } } + private class Editing + { + private readonly DalamudPluginInterface _pi; + + private string _inputPath = string.Empty; + private string _inputPath2 = string.Empty; + private string _outputPath = string.Empty; + private string _outputPath2 = string.Empty; + + private TextureType _typeSelector; + private bool _mipMaps = true; + + private Task? _task1; + private Task? _task2; + + public Editing(DalamudPluginInterface pi) + => _pi = pi; + + public void Draw() + { + using var _ = ImRaii.TreeNode("Editing"); + if (!_) + return; + + ImGui.InputTextWithHint("##inputPath", "Input Texture Path...", ref _inputPath, 256); + ImGui.InputTextWithHint("##outputPath", "Output Texture Path...", ref _outputPath, 256); + ImGui.InputTextWithHint("##inputPath2", "Input Texture Path 2...", ref _inputPath2, 256); + ImGui.InputTextWithHint("##outputPath2", "Output Texture Path 2...", ref _outputPath2, 256); + TypeCombo(); + ImGui.Checkbox("Add MipMaps", ref _mipMaps); + + using var table = ImRaii.Table("...", 3, ImGuiTableFlags.SizingFixedFit); + if (!table) + return; + + DrawIntro(Ipc.ConvertTextureFile.Label, "Convert Texture 1"); + if (ImGuiUtil.DrawDisabledButton("Save 1", Vector2.Zero, string.Empty, _task1 is { IsCompleted: false })) + _task1 = Ipc.ConvertTextureFile.Subscriber(_pi).Invoke(_inputPath, _outputPath, _typeSelector, _mipMaps); + ImGui.SameLine(); + ImGui.TextUnformatted(_task1 == null ? "Not Initiated" : _task1.Status.ToString()); + if (ImGui.IsItemHovered() && _task1?.Status == TaskStatus.Faulted) + ImGui.SetTooltip(_task1.Exception?.ToString()); + + DrawIntro(Ipc.ConvertTextureFile.Label, "Convert Texture 2"); + if (ImGuiUtil.DrawDisabledButton("Save 2", Vector2.Zero, string.Empty, _task2 is { IsCompleted: false })) + _task2 = Ipc.ConvertTextureFile.Subscriber(_pi).Invoke(_inputPath2, _outputPath2, _typeSelector, _mipMaps); + ImGui.SameLine(); + ImGui.TextUnformatted(_task2 == null ? "Not Initiated" : _task2.Status.ToString()); + if (ImGui.IsItemHovered() && _task2?.Status == TaskStatus.Faulted) + ImGui.SetTooltip(_task2.Exception?.ToString()); + } + + private void TypeCombo() + { + using var combo = ImRaii.Combo("Convert To", _typeSelector.ToString()); + if (!combo) + return; + + foreach (var value in Enum.GetValues()) + { + if (ImGui.Selectable(value.ToString(), _typeSelector == value)) + _typeSelector = value; + } + } + } + private class Temporary { private readonly DalamudPluginInterface _pi; diff --git a/Penumbra/Api/PenumbraApi.cs b/Penumbra/Api/PenumbraApi.cs index 9b7e6410..140a928b 100644 --- a/Penumbra/Api/PenumbraApi.cs +++ b/Penumbra/Api/PenumbraApi.cs @@ -8,11 +8,13 @@ using Penumbra.Interop.Structs; using Penumbra.Meta.Manipulations; using Penumbra.Mods; using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Runtime.CompilerServices; +using System.Threading.Tasks; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using Penumbra.Api.Enums; using Penumbra.GameData.Actors; @@ -23,15 +25,17 @@ using Penumbra.String.Classes; using Penumbra.Services; using Penumbra.Collections.Manager; using Penumbra.Communication; +using Penumbra.Import.Textures; using Penumbra.Interop.Services; using Penumbra.UI; +using TextureType = Penumbra.Api.Enums.TextureType; namespace Penumbra.Api; public class PenumbraApi : IDisposable, IPenumbraApi { public (int, int) ApiVersion - => (4, 20); + => (4, 21); public event Action? PreSettingsPanelDraw { @@ -124,12 +128,13 @@ public class PenumbraApi : IDisposable, IPenumbraApi private RedrawService _redrawService; private ModFileSystem _modFileSystem; private ConfigWindow _configWindow; + private TextureManager _textureManager; public unsafe PenumbraApi(CommunicatorService communicator, ModManager modManager, ResourceLoader resourceLoader, Configuration config, CollectionManager collectionManager, DalamudServices dalamud, TempCollectionManager tempCollections, TempModManager tempMods, ActorService actors, CollectionResolver collectionResolver, CutsceneService cutsceneService, ModImportManager modImportManager, CollectionEditor collectionEditor, RedrawService redrawService, ModFileSystem modFileSystem, - ConfigWindow configWindow) + ConfigWindow configWindow, TextureManager textureManager) { _communicator = communicator; _modManager = modManager; @@ -147,6 +152,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi _redrawService = redrawService; _modFileSystem = modFileSystem; _configWindow = configWindow; + _textureManager = textureManager; _lumina = _dalamud.GameData.GameData; _resourceLoader.ResourceLoaded += OnResourceLoaded; @@ -179,6 +185,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi _redrawService = null!; _modFileSystem = null!; _configWindow = null!; + _textureManager = null!; } public event ChangedItemClick? ChangedItemClicked @@ -992,6 +999,39 @@ public class PenumbraApi : IDisposable, IPenumbraApi return Functions.ToCompressedBase64(set, MetaManipulation.CurrentVersion); } + public Task ConvertTextureFile(string inputFile, string outputFile, TextureType textureType, bool mipMaps) + => textureType switch + { + TextureType.Png => _textureManager.SavePng(inputFile, outputFile), + TextureType.AsIsTex => _textureManager.SaveAs(CombinedTexture.TextureSaveType.AsIs, mipMaps, true, inputFile, outputFile), + TextureType.AsIsDds => _textureManager.SaveAs(CombinedTexture.TextureSaveType.AsIs, mipMaps, false, inputFile, outputFile), + TextureType.RgbaTex => _textureManager.SaveAs(CombinedTexture.TextureSaveType.Bitmap, mipMaps, true, inputFile, outputFile), + TextureType.RgbaDds => _textureManager.SaveAs(CombinedTexture.TextureSaveType.Bitmap, mipMaps, false, inputFile, outputFile), + TextureType.Bc3Tex => _textureManager.SaveAs(CombinedTexture.TextureSaveType.BC3, mipMaps, true, inputFile, outputFile), + TextureType.Bc3Dds => _textureManager.SaveAs(CombinedTexture.TextureSaveType.BC3, mipMaps, false, inputFile, outputFile), + TextureType.Bc7Tex => _textureManager.SaveAs(CombinedTexture.TextureSaveType.BC7, mipMaps, true, inputFile, outputFile), + TextureType.Bc7Dds => _textureManager.SaveAs(CombinedTexture.TextureSaveType.BC7, mipMaps, false, inputFile, outputFile), + _ => Task.FromException(new Exception($"Invalid input value {textureType}.")), + }; + + // @formatter:off + public Task ConvertTextureData(byte[] rgbaData, int width, string outputFile, TextureType textureType, bool mipMaps) + => textureType switch + { + TextureType.Png => _textureManager.SavePng(new BaseImage(), outputFile, rgbaData, width, rgbaData.Length / 4 / width), + TextureType.AsIsTex => _textureManager.SaveAs(CombinedTexture.TextureSaveType.AsIs, mipMaps, true, new BaseImage(), outputFile, rgbaData, width, rgbaData.Length / 4 / width), + TextureType.AsIsDds => _textureManager.SaveAs(CombinedTexture.TextureSaveType.AsIs, mipMaps, false, new BaseImage(), outputFile, rgbaData, width, rgbaData.Length / 4 / width), + TextureType.RgbaTex => _textureManager.SaveAs(CombinedTexture.TextureSaveType.Bitmap, mipMaps, true, new BaseImage(), outputFile, rgbaData, width, rgbaData.Length / 4 / width), + TextureType.RgbaDds => _textureManager.SaveAs(CombinedTexture.TextureSaveType.Bitmap, mipMaps, false, new BaseImage(), outputFile, rgbaData, width, rgbaData.Length / 4 / width), + TextureType.Bc3Tex => _textureManager.SaveAs(CombinedTexture.TextureSaveType.BC3, mipMaps, true, new BaseImage(), outputFile, rgbaData, width, rgbaData.Length / 4 / width), + TextureType.Bc3Dds => _textureManager.SaveAs(CombinedTexture.TextureSaveType.BC3, mipMaps, false, new BaseImage(), outputFile, rgbaData, width, rgbaData.Length / 4 / width), + TextureType.Bc7Tex => _textureManager.SaveAs(CombinedTexture.TextureSaveType.BC7, mipMaps, true, new BaseImage(), outputFile, rgbaData, width, rgbaData.Length / 4 / width), + TextureType.Bc7Dds => _textureManager.SaveAs(CombinedTexture.TextureSaveType.BC7, mipMaps, false, new BaseImage(), outputFile, rgbaData, width, rgbaData.Length / 4 / width), + _ => Task.FromException(new Exception($"Invalid input value {textureType}.")), + }; + // @formatter:on + + // TODO: cleanup when incrementing API public string GetMetaManipulations(string characterName) => GetMetaManipulations(characterName, ushort.MaxValue); diff --git a/Penumbra/Api/PenumbraIpcProviders.cs b/Penumbra/Api/PenumbraIpcProviders.cs index 7ccd7e20..73f87b94 100644 --- a/Penumbra/Api/PenumbraIpcProviders.cs +++ b/Penumbra/Api/PenumbraIpcProviders.cs @@ -3,6 +3,7 @@ using Dalamud.Plugin; using Penumbra.GameData.Enums; using System; using System.Collections.Generic; +using System.Threading.Tasks; using Penumbra.Api.Enums; using Penumbra.Api.Helpers; using Penumbra.Collections.Manager; @@ -105,6 +106,10 @@ public class PenumbraIpcProviders : IDisposable internal readonly EventProvider ModSettingChanged; internal readonly FuncProvider CopyModSettings; + // Editing + internal readonly FuncProvider ConvertTextureFile; + internal readonly FuncProvider ConvertTextureData; + // Temporary internal readonly FuncProvider CreateTemporaryCollection; internal readonly FuncProvider RemoveTemporaryCollection; @@ -219,6 +224,10 @@ public class PenumbraIpcProviders : IDisposable () => Api.ModSettingChanged -= ModSettingChangedEvent); CopyModSettings = Ipc.CopyModSettings.Provider(pi, Api.CopyModSettings); + // Editing + ConvertTextureFile = Ipc.ConvertTextureFile.Provider(pi, Api.ConvertTextureFile); + ConvertTextureData = Ipc.ConvertTextureData.Provider(pi, Api.ConvertTextureData); + // Temporary CreateTemporaryCollection = Ipc.CreateTemporaryCollection.Provider(pi, Api.CreateTemporaryCollection); RemoveTemporaryCollection = Ipc.RemoveTemporaryCollection.Provider(pi, Api.RemoveTemporaryCollection); @@ -335,6 +344,10 @@ public class PenumbraIpcProviders : IDisposable RemoveTemporaryModAll.Dispose(); RemoveTemporaryMod.Dispose(); + // Editing + ConvertTextureFile.Dispose(); + ConvertTextureData.Dispose(); + Disposed.Invoke(); Disposed.Dispose(); } diff --git a/Penumbra/Import/Textures/CombinedTexture.cs b/Penumbra/Import/Textures/CombinedTexture.cs index 99303234..c26cb900 100644 --- a/Penumbra/Import/Textures/CombinedTexture.cs +++ b/Penumbra/Import/Textures/CombinedTexture.cs @@ -1,5 +1,6 @@ using System; using System.Numerics; +using System.Threading.Tasks; namespace Penumbra.Import.Textures; @@ -29,7 +30,7 @@ public partial class CombinedTexture : IDisposable private readonly Texture _centerStorage = new(); - public Guid SaveGuid { get; private set; } = Guid.Empty; + public Task SaveTask { get; private set; } = Task.CompletedTask; public bool IsLoaded => _mode != Mode.Empty; @@ -55,7 +56,7 @@ public partial class CombinedTexture : IDisposable if (!IsLoaded || _current == null) return; - SaveGuid = textures.SavePng(_current.BaseImage, path, _current.RgbaPixels, _current.TextureWrap!.Width, _current.TextureWrap!.Height); + SaveTask = textures.SavePng(_current.BaseImage, path, _current.RgbaPixels, _current.TextureWrap!.Width, _current.TextureWrap!.Height); } private void SaveAs(TextureManager textures, string path, TextureSaveType type, bool mipMaps, bool writeTex) @@ -63,7 +64,7 @@ public partial class CombinedTexture : IDisposable if (!IsLoaded || _current == null) return; - SaveGuid = textures.SaveAs(type, mipMaps, writeTex, _current.BaseImage, path, _current.RgbaPixels, _current.TextureWrap!.Width, + SaveTask = textures.SaveAs(type, mipMaps, writeTex, _current.BaseImage, path, _current.RgbaPixels, _current.TextureWrap!.Width, _current.TextureWrap!.Height); } @@ -133,7 +134,7 @@ public partial class CombinedTexture : IDisposable { _centerStorage.Dispose(); _current = null; - SaveGuid = Guid.Empty; + SaveTask = Task.CompletedTask; _mode = Mode.Empty; } } diff --git a/Penumbra/Import/Textures/TextureManager.cs b/Penumbra/Import/Textures/TextureManager.cs index 9ac503df..76604e84 100644 --- a/Penumbra/Import/Textures/TextureManager.cs +++ b/Penumbra/Import/Textures/TextureManager.cs @@ -1,8 +1,11 @@ using System; +using System.Collections.Concurrent; +using System.Collections.Generic; using System.IO; using System.Linq; using System.Numerics; using System.Threading; +using System.Threading.Tasks; using Dalamud.Data; using Dalamud.Interface; using ImGuiScene; @@ -11,100 +14,71 @@ using OtterGui.Log; using OtterGui.Tasks; using OtterTex; using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Formats.Png; using SixLabors.ImageSharp.PixelFormats; +using Swan; using Image = SixLabors.ImageSharp.Image; namespace Penumbra.Import.Textures; -public sealed class TextureManager : AsyncTaskManager +public sealed class TextureManager : SingleTaskQueue, IDisposable { + private readonly Logger _logger; private readonly UiBuilder _uiBuilder; private readonly DataManager _gameData; - public TextureManager(Logger logger, UiBuilder uiBuilder, DataManager gameData) - : base(logger) + private readonly ConcurrentDictionary _tasks = new(); + private bool _disposed = false; + + public TextureManager(UiBuilder uiBuilder, DataManager gameData, Logger logger) { _uiBuilder = uiBuilder; _gameData = gameData; + _logger = logger; } - public Guid SavePng(string input, string output) + public IReadOnlyDictionary Tasks + => _tasks; + + public void Dispose() + { + _disposed = true; + foreach (var (_, cancel) in _tasks.Values.ToArray()) + cancel.Cancel(); + _tasks.Clear(); + } + + public Task SavePng(string input, string output) => Enqueue(new SavePngAction(this, input, output)); - public Guid SavePng(BaseImage image, string path, byte[]? rgba = null, int width = 0, int height = 0) + public Task SavePng(BaseImage image, string path, byte[]? rgba = null, int width = 0, int height = 0) => Enqueue(new SavePngAction(this, image, path, rgba, width, height)); - public Guid SaveAs(CombinedTexture.TextureSaveType type, bool mipMaps, bool asTex, string input, string output) + public Task SaveAs(CombinedTexture.TextureSaveType type, bool mipMaps, bool asTex, string input, string output) => Enqueue(new SaveAsAction(this, type, mipMaps, asTex, input, output)); - public Guid SaveAs(CombinedTexture.TextureSaveType type, bool mipMaps, bool asTex, BaseImage image, string path, byte[]? rgba = null, + public Task SaveAs(CombinedTexture.TextureSaveType type, bool mipMaps, bool asTex, BaseImage image, string path, byte[]? rgba = null, int width = 0, int height = 0) => Enqueue(new SaveAsAction(this, type, mipMaps, asTex, image, path, rgba, width, height)); - private readonly struct ImageInputData + private Task Enqueue(IAction action) { - private readonly string? _inputPath; + if (_disposed) + return Task.FromException(new ObjectDisposedException(nameof(TextureManager))); - private readonly BaseImage _image; - private readonly byte[]? _rgba; - private readonly int _width; - private readonly int _height; - - public ImageInputData(string inputPath) + Task t; + lock (_tasks) { - _inputPath = inputPath; - _image = new BaseImage(); - _rgba = null; - _width = 0; - _height = 0; + t = _tasks.GetOrAdd(action, a => + { + var token = new CancellationTokenSource(); + var task = Enqueue(a, token.Token); + task.ContinueWith(_ => _tasks.TryRemove(a, out var unused), CancellationToken.None); + return (task, token); + }).Item1; } - public ImageInputData(BaseImage image, byte[]? rgba = null, int width = 0, int height = 0) - { - _inputPath = null; - _image = image.Width == 0 || image.Height == 0 ? new BaseImage() : image; - _rgba = rgba?.ToArray(); - _width = width; - _height = height; - } - - public (BaseImage Image, byte[]? Rgba, int Width, int Height) GetData(TextureManager textures) - { - if (_inputPath == null) - return (_image, _rgba, _width, _height); - - if (!File.Exists(_inputPath)) - throw new FileNotFoundException($"Input texture file {_inputPath} not Found.", _inputPath); - - var (image, _) = textures.Load(_inputPath); - return (image, null, 0, 0); - } - - public bool Equals(ImageInputData rhs) - { - if (_inputPath != null) - return string.Equals(_inputPath, rhs._inputPath, StringComparison.OrdinalIgnoreCase); - - if (rhs._inputPath != null) - return false; - - if (_image.Image != null) - return ReferenceEquals(_image.Image, rhs._image.Image); - - return _width == rhs._width && _height == rhs._height && _rgba != null && rhs._rgba != null && _rgba.SequenceEqual(rhs._rgba); - } - - public override string ToString() - => _inputPath - ?? _image.Type switch - { - TextureType.Unknown => $"Custom {_width} x {_height} RGBA Image", - TextureType.Dds => $"Custom {_width} x {_height} {_image.Format} Image", - TextureType.Tex => $"Custom {_width} x {_height} {_image.Format} Image", - TextureType.Png => $"Custom {_width} x {_height} .png Image", - TextureType.Bitmap => $"Custom {_width} x {_height} RGBA Image", - _ => "Unknown Image", - }; + return t; } private class SavePngAction : IAction @@ -129,7 +103,7 @@ public sealed class TextureManager : AsyncTaskManager public void Execute(CancellationToken cancel) { - _textures.Logger.Information($"[{nameof(TextureManager)}] Saving {_input} as .png to {_outputPath}..."); + _textures._logger.Information($"[{nameof(TextureManager)}] Saving {_input} as .png to {_outputPath}..."); var (image, rgba, width, height) = _input.GetData(_textures); cancel.ThrowIfCancellationRequested(); Image? png = null; @@ -144,9 +118,12 @@ public sealed class TextureManager : AsyncTaskManager } cancel.ThrowIfCancellationRequested(); - png?.SaveAsync(_outputPath, cancel).Wait(cancel); + png?.SaveAsync(_outputPath, new PngEncoder() { CompressionLevel = PngCompressionLevel.NoCompression }, cancel).Wait(cancel); } + public override string ToString() + => $"{_input} to {_outputPath} PNG"; + public bool Equals(IAction? other) { if (other is not SavePngAction rhs) @@ -154,6 +131,9 @@ public sealed class TextureManager : AsyncTaskManager return string.Equals(_outputPath, rhs._outputPath, StringComparison.OrdinalIgnoreCase) && _input.Equals(rhs._input); } + + public override int GetHashCode() + => HashCode.Combine(_outputPath.ToLowerInvariant(), _input); } private class SaveAsAction : IAction @@ -177,8 +157,7 @@ public sealed class TextureManager : AsyncTaskManager } public SaveAsAction(TextureManager textures, CombinedTexture.TextureSaveType type, bool mipMaps, bool asTex, BaseImage image, - string path, - byte[]? rgba = null, int width = 0, int height = 0) + string path, byte[]? rgba = null, int width = 0, int height = 0) { _textures = textures; _input = new ImageInputData(image, rgba, width, height); @@ -190,7 +169,7 @@ public sealed class TextureManager : AsyncTaskManager public void Execute(CancellationToken cancel) { - _textures.Logger.Information( + _textures._logger.Information( $"[{nameof(TextureManager)}] Saving {_input} as {_type} {(_asTex ? ".tex" : ".dds")} file{(_mipMaps ? " with mip maps" : string.Empty)} to {_outputPath}..."); var (image, rgba, width, height) = _input.GetData(_textures); if (image.Type is TextureType.Unknown) @@ -220,6 +199,9 @@ public sealed class TextureManager : AsyncTaskManager dds.AsDds!.SaveDDS(_outputPath); } + public override string ToString() + => $"{_input} to {_outputPath} {_type} {(_asTex ? "TEX" : "DDS")}{(_mipMaps ? " with MipMaps" : string.Empty)}"; + public bool Equals(IAction? other) { if (other is not SaveAsAction rhs) @@ -231,6 +213,9 @@ public sealed class TextureManager : AsyncTaskManager && string.Equals(_outputPath, rhs._outputPath, StringComparison.OrdinalIgnoreCase) && _input.Equals(rhs._input); } + + public override int GetHashCode() + => HashCode.Combine(_outputPath.ToLowerInvariant(), _type, _mipMaps, _asTex, _input); } /// Load a texture wrap for a given image. @@ -435,4 +420,73 @@ public sealed class TextureManager : AsyncTaskManager header.Write(w); w.Write(input.Pixels); } + + private readonly struct ImageInputData + { + private readonly string? _inputPath; + + private readonly BaseImage _image; + private readonly byte[]? _rgba; + private readonly int _width; + private readonly int _height; + + public ImageInputData(string inputPath) + { + _inputPath = inputPath; + _image = new BaseImage(); + _rgba = null; + _width = 0; + _height = 0; + } + + public ImageInputData(BaseImage image, byte[]? rgba = null, int width = 0, int height = 0) + { + _inputPath = null; + _image = image.Width == 0 || image.Height == 0 ? new BaseImage() : image; + _rgba = rgba?.ToArray(); + _width = width; + _height = height; + } + + public (BaseImage Image, byte[]? Rgba, int Width, int Height) GetData(TextureManager textures) + { + if (_inputPath == null) + return (_image, _rgba, _width, _height); + + if (!File.Exists(_inputPath)) + throw new FileNotFoundException($"Input texture file {_inputPath} not Found.", _inputPath); + + var (image, _) = textures.Load(_inputPath); + return (image, null, 0, 0); + } + + public bool Equals(ImageInputData rhs) + { + if (_inputPath != null) + return string.Equals(_inputPath, rhs._inputPath, StringComparison.OrdinalIgnoreCase); + + if (rhs._inputPath != null) + return false; + + if (_image.Image != null) + return ReferenceEquals(_image.Image, rhs._image.Image); + + return _width == rhs._width && _height == rhs._height && _rgba != null && rhs._rgba != null && _rgba.SequenceEqual(rhs._rgba); + } + + public override string ToString() + => _inputPath + ?? _image.Type switch + { + TextureType.Unknown => $"Custom {_width} x {_height} RGBA Image", + TextureType.Dds => $"Custom {_width} x {_height} {_image.Format} Image", + TextureType.Tex => $"Custom {_width} x {_height} {_image.Format} Image", + TextureType.Png => $"Custom {_width} x {_height} .png Image", + TextureType.Bitmap => $"Custom {_width} x {_height} RGBA Image", + _ => "Unknown Image", + }; + + public override int GetHashCode() + => _inputPath != null ? _inputPath.ToLowerInvariant().GetHashCode() : HashCode.Combine(_width, _height); + } } diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs index 6fd7b130..07607d11 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs @@ -2,6 +2,7 @@ using System; using System.IO; using System.Linq; using System.Numerics; +using System.Threading.Tasks; using ImGuiNET; using OtterGui; using OtterGui.Raii; @@ -144,7 +145,7 @@ public partial class ModEditWindow _left.Format is DXGIFormat.BC7Typeless or DXGIFormat.BC7UNorm or DXGIFormat.BC7UNormSRGB)) { _center.SaveAsTex(_textures, _left.Path, CombinedTexture.TextureSaveType.BC7, _left.MipMaps > 1); - ReloadConvertedSubscribe(_left.Path, _center.SaveGuid); + AddReloadTask(_left.Path); } ImGui.SameLine(); @@ -153,7 +154,7 @@ public partial class ModEditWindow _left.Format is DXGIFormat.BC3Typeless or DXGIFormat.BC3UNorm or DXGIFormat.BC3UNormSRGB)) { _center.SaveAsTex(_textures, _left.Path, CombinedTexture.TextureSaveType.BC3, _left.MipMaps > 1); - ReloadConvertedSubscribe(_left.Path, _center.SaveGuid); + AddReloadTask(_left.Path); } ImGui.SameLine(); @@ -162,7 +163,7 @@ public partial class ModEditWindow _left.Format is DXGIFormat.B8G8R8A8UNorm or DXGIFormat.B8G8R8A8Typeless or DXGIFormat.B8G8R8A8UNormSRGB)) { _center.SaveAsTex(_textures, _left.Path, CombinedTexture.TextureSaveType.Bitmap, _left.MipMaps > 1); - ReloadConvertedSubscribe(_left.Path, _center.SaveGuid); + AddReloadTask(_left.Path); } } else @@ -173,18 +174,20 @@ public partial class ModEditWindow ImGui.NewLine(); } - if (_center.SaveGuid != Guid.Empty) + switch (_center.SaveTask.Status) { - var state = _textures.GetState(_center.SaveGuid, out var saveException, out _, out _); - if (saveException != null) + case TaskStatus.WaitingForActivation: + case TaskStatus.WaitingToRun: + case TaskStatus.Running: + ImGui.TextUnformatted("Computing..."); + break; + case TaskStatus.Canceled: + case TaskStatus.Faulted: { ImGui.TextUnformatted("Could not save file:"); using var color = ImRaii.PushColor(ImGuiCol.Text, 0xFF0000FF); - ImGuiUtil.TextWrapped(saveException.ToString()); - } - else if (state == ActionState.Running) - { - ImGui.TextUnformatted("Computing..."); + ImGuiUtil.TextWrapped(_center.SaveTask.Exception?.ToString() ?? "Unknown Error"); + break; } } @@ -193,23 +196,18 @@ public partial class ModEditWindow _center.Draw(_textures, imageSize); } - - private void ReloadConvertedSubscribe(string path, Guid guid) + private void AddReloadTask(string path) { - void Reload(Guid eventGuid, ActionState state, Exception? ex) + _center.SaveTask.ContinueWith(t => { - if (guid != eventGuid) + if (!t.IsCompletedSuccessfully) return; if (_left.Path != path) return; - if (state is ActionState.Succeeded) - _dalamud.Framework.RunOnFrameworkThread(() => _left.Reload(_textures)); - _textures.Finished -= Reload; - } - - _textures.Finished += Reload; + _dalamud.Framework.RunOnFrameworkThread(() => _left.Reload(_textures)); + }); } private Vector2 GetChildWidth() diff --git a/Penumbra/UI/Tabs/DebugTab.cs b/Penumbra/UI/Tabs/DebugTab.cs index 066aec6c..a48fd714 100644 --- a/Penumbra/UI/Tabs/DebugTab.cs +++ b/Penumbra/UI/Tabs/DebugTab.cs @@ -19,6 +19,7 @@ using Penumbra.Collections.Manager; using Penumbra.GameData.Actors; using Penumbra.GameData.Files; using Penumbra.Import.Structs; +using Penumbra.Import.Textures; using Penumbra.Interop.ResourceLoading; using Penumbra.Interop.PathResolving; using Penumbra.Interop.Structs; @@ -61,13 +62,15 @@ public class DebugTab : Window, ITab private readonly ModImportManager _modImporter; private readonly ImportPopup _importPopup; private readonly FrameworkManager _framework; + private readonly TextureManager _textureManager; public DebugTab(StartTracker timer, PerformanceTracker performance, Configuration config, CollectionManager collectionManager, ValidityChecker validityChecker, ModManager modManager, HttpApi httpApi, ActorService actorService, DalamudServices dalamud, StainService stains, CharacterUtility characterUtility, ResidentResourceManager residentResources, ResourceManagerService resourceManager, PenumbraIpcProviders ipc, CollectionResolver collectionResolver, DrawObjectState drawObjectState, PathState pathState, SubfileHelper subfileHelper, IdentifiedCollectionCache identifiedCollectionCache, - CutsceneService cutsceneService, ModImportManager modImporter, ImportPopup importPopup, FrameworkManager framework) + CutsceneService cutsceneService, ModImportManager modImporter, ImportPopup importPopup, FrameworkManager framework, + TextureManager textureManager) : base("Penumbra Debug Window", ImGuiWindowFlags.NoCollapse, false) { IsOpen = true; @@ -99,6 +102,7 @@ public class DebugTab : Window, ITab _modImporter = modImporter; _importPopup = importPopup; _framework = framework; + _textureManager = textureManager; } public ReadOnlySpan Label @@ -147,14 +151,15 @@ public class DebugTab : Window, ITab private void DrawCollectionCaches() { - if (!ImGui.CollapsingHeader($"Collections ({_collectionManager.Caches.Count}/{_collectionManager.Storage.Count - 1} Caches)###Collections")) + if (!ImGui.CollapsingHeader( + $"Collections ({_collectionManager.Caches.Count}/{_collectionManager.Storage.Count - 1} Caches)###Collections")) return; foreach (var collection in _collectionManager.Storage) { if (collection.HasCache) { - using var color = ImRaii.PushColor(ImGuiCol.Text, ColorId.FolderExpanded.Value()); + using var color = PushColor(ImGuiCol.Text, ColorId.FolderExpanded.Value()); using var node = TreeNode($"{collection.AnonymizedName} (Change Counter {collection.ChangeCounter})"); if (!node) continue; @@ -177,8 +182,9 @@ public class DebugTab : Window, ITab } else { - using var color = ImRaii.PushColor(ImGuiCol.Text, ColorId.UndefinedMod.Value()); - TreeNode($"{collection.AnonymizedName} (Change Counter {collection.ChangeCounter})", ImGuiTreeNodeFlags.Bullet | ImGuiTreeNodeFlags.Leaf).Dispose(); + using var color = PushColor(ImGuiCol.Text, ColorId.UndefinedMod.Value()); + TreeNode($"{collection.AnonymizedName} (Change Counter {collection.ChangeCounter})", + ImGuiTreeNodeFlags.Bullet | ImGuiTreeNodeFlags.Leaf).Dispose(); } } } @@ -293,6 +299,20 @@ public class DebugTab : Window, ITab } } } + + using (var tree = TreeNode($"Texture Manager {_textureManager.Tasks.Count}###Texture Manager")) + { + if (tree) + { + using var table = Table("##Tasks", 2, ImGuiTableFlags.RowBg); + if (table) + foreach (var task in _textureManager.Tasks) + { + ImGuiUtil.DrawTableColumn(task.Key.ToString()!); + ImGuiUtil.DrawTableColumn(task.Value.Item1.Status.ToString()); + } + } + } } private void DrawPerformanceTab() @@ -500,7 +520,7 @@ public class DebugTab : Window, ITab if (agent->Data != null) { - using var table = Table("###PBannerTable", 2, ImGuiTableFlags.SizingFixedFit); + using var table = Table("###PBannerTable", 2, ImGuiTableFlags.SizingFixedFit); if (table) for (var i = 0; i < 8; ++i) {