diff --git a/OtterGui b/OtterGui index e3d26f16..8d61845c 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit e3d26f16234a4295bf3c7802d87ce43293c6ffe0 +Subproject commit 8d61845cd900fc0a3b58d475c43303b13c1165f4 diff --git a/Penumbra/Import/Textures/BaseImage.cs b/Penumbra/Import/Textures/BaseImage.cs new file mode 100644 index 00000000..f0f6a47e --- /dev/null +++ b/Penumbra/Import/Textures/BaseImage.cs @@ -0,0 +1,111 @@ +using System; +using System.Numerics; +using Lumina.Data.Files; +using OtterTex; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.PixelFormats; + +namespace Penumbra.Import.Textures; + +public readonly struct BaseImage : IDisposable +{ + public readonly object? Image; + + public BaseImage(ScratchImage scratch) + => Image = scratch; + + public BaseImage(Image image) + => Image = image; + + public static implicit operator BaseImage(ScratchImage scratch) + => new(scratch); + + public static implicit operator BaseImage(Image img) + => new(img); + + public ScratchImage? AsDds + => Image as ScratchImage; + + public Image? AsPng + => Image as Image; + + public TexFile? AsTex + => Image as TexFile; + + public TextureType Type + => Image switch + { + null => TextureType.Unknown, + ScratchImage => TextureType.Dds, + Image => TextureType.Png, + _ => TextureType.Unknown, + }; + + public void Dispose() + => (Image as IDisposable)?.Dispose(); + + /// Obtain RGBA pixel data for the given image (not including any mip maps.) + public (byte[] Rgba, int Width, int Height) GetPixelData() + { + switch (Image) + { + case null: return (Array.Empty(), 0, 0); + case ScratchImage scratch: + { + var rgba = scratch.GetRGBA(out var f).ThrowIfError(f); + return (rgba.Pixels[..(f.Meta.Width * f.Meta.Height * (f.Meta.Format.BitsPerPixel() / 8))].ToArray(), f.Meta.Width, + f.Meta.Height); + } + case Image img: + { + var ret = new byte[img.Height * img.Width * 4]; + img.CopyPixelDataTo(ret); + return (ret, img.Width, img.Height); + } + default: return (Array.Empty(), 0, 0); + } + } + + public (int Width, int Height) Dimensions + => Image switch + { + null => (0, 0), + ScratchImage scratch => (scratch.Meta.Width, scratch.Meta.Height), + Image img => (img.Width, img.Height), + _ => (0, 0), + }; + + public int Width + => Dimensions.Width; + + public int Height + => Dimensions.Height; + + public Vector2 ImageSize + { + get + { + var (width, height) = Dimensions; + return new Vector2(width, height); + } + } + + public DXGIFormat Format + => Image switch + { + null => DXGIFormat.Unknown, + ScratchImage s => s.Meta.Format, + TexFile t => t.Header.Format.ToDXGI(), + Image => DXGIFormat.B8G8R8X8UNorm, + _ => DXGIFormat.Unknown, + }; + + public int MipMaps + => Image switch + { + null => 0, + ScratchImage s => s.Meta.MipLevels, + TexFile t => t.Header.MipLevels, + _ => 1, + }; +} diff --git a/Penumbra/Import/Textures/CombinedTexture.Manipulation.cs b/Penumbra/Import/Textures/CombinedTexture.Manipulation.cs index 16bd6dfe..a32b9578 100644 --- a/Penumbra/Import/Textures/CombinedTexture.Manipulation.cs +++ b/Penumbra/Import/Textures/CombinedTexture.Manipulation.cs @@ -16,14 +16,13 @@ public partial class CombinedTexture private bool _invertLeft = false; private bool _invertRight = false; private int _offsetX = 0; - private int _offsetY = 0; - + private int _offsetY = 0; private Vector4 DataLeft( int offset ) - => CappedVector( _left.RGBAPixels, offset, _multiplierLeft, _invertLeft ); + => CappedVector( _left.RgbaPixels, offset, _multiplierLeft, _invertLeft ); private Vector4 DataRight( int offset ) - => CappedVector( _right.RGBAPixels, offset, _multiplierRight, _invertRight ); + => CappedVector( _right.RgbaPixels, offset, _multiplierRight, _invertRight ); private Vector4 DataRight( int x, int y ) { @@ -35,7 +34,7 @@ public partial class CombinedTexture } var offset = ( y * _right.TextureWrap!.Width + x ) * 4; - return CappedVector( _right.RGBAPixels, offset, _multiplierRight, _invertRight ); + return CappedVector( _right.RgbaPixels, offset, _multiplierRight, _invertRight ); } private void AddPixelsMultiplied( int y, ParallelLoopState _ ) @@ -49,10 +48,10 @@ public partial class CombinedTexture var rgba = alpha == 0 ? new Rgba32() : new Rgba32( ( ( right * right.W + left * left.W * ( 1 - right.W ) ) / alpha ) with { W = alpha } ); - _centerStorage.RGBAPixels[ offset ] = rgba.R; - _centerStorage.RGBAPixels[ offset + 1 ] = rgba.G; - _centerStorage.RGBAPixels[ offset + 2 ] = rgba.B; - _centerStorage.RGBAPixels[ offset + 3 ] = rgba.A; + _centerStorage.RgbaPixels[ offset ] = rgba.R; + _centerStorage.RgbaPixels[ offset + 1 ] = rgba.G; + _centerStorage.RgbaPixels[ offset + 2 ] = rgba.B; + _centerStorage.RgbaPixels[ offset + 3 ] = rgba.A; } } @@ -63,10 +62,10 @@ public partial class CombinedTexture var offset = ( _left.TextureWrap!.Width * y + x ) * 4; var left = DataLeft( offset ); var rgba = new Rgba32( left ); - _centerStorage.RGBAPixels[ offset ] = rgba.R; - _centerStorage.RGBAPixels[ offset + 1 ] = rgba.G; - _centerStorage.RGBAPixels[ offset + 2 ] = rgba.B; - _centerStorage.RGBAPixels[ offset + 3 ] = rgba.A; + _centerStorage.RgbaPixels[ offset ] = rgba.R; + _centerStorage.RgbaPixels[ offset + 1 ] = rgba.G; + _centerStorage.RgbaPixels[ offset + 2 ] = rgba.B; + _centerStorage.RgbaPixels[ offset + 3 ] = rgba.A; } } @@ -77,10 +76,10 @@ public partial class CombinedTexture var offset = ( _right.TextureWrap!.Width * y + x ) * 4; var left = DataRight( offset ); var rgba = new Rgba32( left ); - _centerStorage.RGBAPixels[ offset ] = rgba.R; - _centerStorage.RGBAPixels[ offset + 1 ] = rgba.G; - _centerStorage.RGBAPixels[ offset + 2 ] = rgba.B; - _centerStorage.RGBAPixels[ offset + 3 ] = rgba.A; + _centerStorage.RgbaPixels[ offset ] = rgba.R; + _centerStorage.RgbaPixels[ offset + 1 ] = rgba.G; + _centerStorage.RgbaPixels[ offset + 2 ] = rgba.B; + _centerStorage.RgbaPixels[ offset + 3 ] = rgba.A; } } @@ -90,8 +89,8 @@ public partial class CombinedTexture var (width, height) = _left.IsLoaded ? ( _left.TextureWrap!.Width, _left.TextureWrap!.Height ) : ( _right.TextureWrap!.Width, _right.TextureWrap!.Height ); - _centerStorage.RGBAPixels = new byte[width * height * 4]; - _centerStorage.Type = Texture.FileType.Bitmap; + _centerStorage.RgbaPixels = new byte[width * height * 4]; + _centerStorage.Type = TextureType.Bitmap; if( _left.IsLoaded ) { Parallel.For( 0, height, _right.IsLoaded ? AddPixelsMultiplied : MultiplyPixelsLeft ); @@ -103,7 +102,6 @@ public partial class CombinedTexture return ( width, height ); } - private static Vector4 CappedVector( IReadOnlyList< byte > bytes, int offset, Matrix4x4 transform, bool invert ) { if( bytes.Count == 0 ) diff --git a/Penumbra/Import/Textures/CombinedTexture.cs b/Penumbra/Import/Textures/CombinedTexture.cs index bf017048..99303234 100644 --- a/Penumbra/Import/Textures/CombinedTexture.cs +++ b/Penumbra/Import/Textures/CombinedTexture.cs @@ -1,14 +1,5 @@ using System; -using System.IO; using System.Numerics; -using Dalamud.Interface; -using Lumina.Data.Files; -using OtterTex; -using SixLabors.ImageSharp; -using SixLabors.ImageSharp.Formats.Png; -using SixLabors.ImageSharp.PixelFormats; -using DalamudUtil = Dalamud.Utility.Util; -using Image = SixLabors.ImageSharp.Image; namespace Penumbra.Import.Textures; @@ -38,170 +29,58 @@ public partial class CombinedTexture : IDisposable private readonly Texture _centerStorage = new(); + public Guid SaveGuid { get; private set; } = Guid.Empty; + public bool IsLoaded => _mode != Mode.Empty; - - public bool IsLeftCopy - => _mode == Mode.LeftCopy; - public Exception? SaveException { get; private set; } = null; + public bool IsLeftCopy + => _mode == Mode.LeftCopy; - public void Draw( UiBuilder builder, Vector2 size ) + public void Draw(TextureManager textures, Vector2 size) { - if( _mode == Mode.Custom && !_centerStorage.IsLoaded ) + if (_mode == Mode.Custom && !_centerStorage.IsLoaded) { - var (width, height) = CombineImage(); - _centerStorage.TextureWrap = - builder.LoadImageRaw( _centerStorage.RGBAPixels, width, height, 4 ); + var (width, height) = CombineImage(); + _centerStorage.TextureWrap = textures.LoadTextureWrap(_centerStorage.RgbaPixels, width, height); } - _current?.Draw( size ); + if (_current != null) + TextureDrawer.Draw(_current, size); } - public void SaveAsPng( string path ) + public void SaveAsPng(TextureManager textures, string path) { - if( !IsLoaded || _current == null ) - { + if (!IsLoaded || _current == null) return; - } - try - { - var image = Image.LoadPixelData< Rgba32 >( _current.RGBAPixels, _current.TextureWrap!.Width, - _current.TextureWrap!.Height ); - image.Save( path, new PngEncoder() { CompressionLevel = PngCompressionLevel.NoCompression } ); - SaveException = null; - } - catch( Exception e ) - { - SaveException = e; - } + SaveGuid = textures.SavePng(_current.BaseImage, path, _current.RgbaPixels, _current.TextureWrap!.Width, _current.TextureWrap!.Height); } - private void SaveAs( string path, TextureSaveType type, bool mipMaps, bool writeTex ) + private void SaveAs(TextureManager textures, string path, TextureSaveType type, bool mipMaps, bool writeTex) { - if( _current == null || _mode == Mode.Empty ) - { + if (!IsLoaded || _current == null) return; - } - try - { - if( _current.BaseImage is not ScratchImage s ) - { - s = ScratchImage.FromRGBA( _current.RGBAPixels, _current.TextureWrap!.Width, - _current.TextureWrap!.Height, out var i ).ThrowIfError( i ); - } - - var tex = type switch - { - TextureSaveType.AsIs => _current.Type is Texture.FileType.Bitmap or Texture.FileType.Png ? CreateUncompressed( s, mipMaps ) : s, - TextureSaveType.Bitmap => CreateUncompressed( s, mipMaps ), - TextureSaveType.BC3 => CreateCompressed( s, mipMaps, false ), - TextureSaveType.BC7 => CreateCompressed( s, mipMaps, true ), - _ => throw new ArgumentOutOfRangeException( nameof( type ), type, null ), - }; - - if( !writeTex ) - { - tex.SaveDDS( path ); - } - else - { - SaveTex( path, tex ); - } - - SaveException = null; - } - catch( Exception e ) - { - SaveException = e; - } + SaveGuid = textures.SaveAs(type, mipMaps, writeTex, _current.BaseImage, path, _current.RgbaPixels, _current.TextureWrap!.Width, + _current.TextureWrap!.Height); } - private static void SaveTex( string path, ScratchImage input ) - { - var header = input.ToTexHeader(); - if( header.Format == TexFile.TextureFormat.Unknown ) - { - throw new Exception( $"Could not save tex file with format {input.Meta.Format}, not convertible to a valid .tex formats." ); - } + public void SaveAsTex(TextureManager textures, string path, TextureSaveType type, bool mipMaps) + => SaveAs(textures, path, type, mipMaps, true); - using var stream = File.Open( path, File.Exists(path) ? FileMode.Truncate : FileMode.CreateNew); - using var w = new BinaryWriter( stream ); - header.Write( w ); - w.Write( input.Pixels ); - } - - private static ScratchImage AddMipMaps( ScratchImage input, bool mipMaps ) - { - if( !mipMaps ) - { - return input; - } - - var numMips = Math.Min( 13, 1 + BitOperations.Log2( ( uint )Math.Max( input.Meta.Width, input.Meta.Height ) ) ); - var ec = input.GenerateMipMaps( out var ret, numMips, ( DalamudUtil.IsLinux() ? FilterFlags.ForceNonWIC : 0 ) | FilterFlags.SeparateAlpha ); - if (ec != ErrorCode.Ok) - { - throw new Exception( $"Could not create the requested {numMips} mip maps, maybe retry with the top-right checkbox unchecked:\n{ec}" ); - } - - return ret; - } - - private static ScratchImage CreateUncompressed( ScratchImage input, bool mipMaps ) - { - if( input.Meta.Format == DXGIFormat.B8G8R8A8UNorm ) - { - return AddMipMaps( input, mipMaps ); - } - - if( input.Meta.Format.IsCompressed() ) - { - input = input.Decompress( DXGIFormat.B8G8R8A8UNorm ); - } - else - { - input = input.Convert( DXGIFormat.B8G8R8A8UNorm ); - } - - return AddMipMaps( input, mipMaps ); - } - - private static ScratchImage CreateCompressed( ScratchImage input, bool mipMaps, bool bc7 ) - { - var format = bc7 ? DXGIFormat.BC7UNorm : DXGIFormat.BC3UNorm; - if( input.Meta.Format == format ) - { - return input; - } - - if( input.Meta.Format.IsCompressed() ) - { - input = input.Decompress( DXGIFormat.B8G8R8A8UNorm ); - } - - input = AddMipMaps( input, mipMaps ); - - return input.Compress( format, CompressFlags.BC7Quick | CompressFlags.Parallel ); - } - - public void SaveAsTex( string path, TextureSaveType type, bool mipMaps ) - => SaveAs( path, type, mipMaps, true ); - - public void SaveAsDds( string path, TextureSaveType type, bool mipMaps ) - => SaveAs( path, type, mipMaps, false ); + public void SaveAsDds(TextureManager textures, string path, TextureSaveType type, bool mipMaps) + => SaveAs(textures, path, type, mipMaps, false); - public CombinedTexture( Texture left, Texture right ) + public CombinedTexture(Texture left, Texture right) { _left = left; _right = right; _left.Loaded += OnLoaded; _right.Loaded += OnLoaded; - OnLoaded( false ); + OnLoaded(false); } public void Dispose() @@ -211,20 +90,20 @@ public partial class CombinedTexture : IDisposable _right.Loaded -= OnLoaded; } - private void OnLoaded( bool _ ) + private void OnLoaded(bool _) => Update(); public void Update() { Clean(); - if( _left.IsLoaded ) + if (_left.IsLoaded) { - if( _right.IsLoaded ) + if (_right.IsLoaded) { _current = _centerStorage; _mode = Mode.Custom; } - else if( !_invertLeft && _multiplierLeft.IsIdentity ) + else if (!_invertLeft && _multiplierLeft.IsIdentity) { _mode = Mode.LeftCopy; _current = _left; @@ -235,9 +114,9 @@ public partial class CombinedTexture : IDisposable _mode = Mode.Custom; } } - else if( _right.IsLoaded ) + else if (_right.IsLoaded) { - if( !_invertRight && _multiplierRight.IsIdentity ) + if (!_invertRight && _multiplierRight.IsIdentity) { _current = _right; _mode = Mode.RightCopy; @@ -254,6 +133,7 @@ public partial class CombinedTexture : IDisposable { _centerStorage.Dispose(); _current = null; + SaveGuid = Guid.Empty; _mode = Mode.Empty; } -} \ No newline at end of file +} diff --git a/Penumbra/Import/Textures/TexFileParser.cs b/Penumbra/Import/Textures/TexFileParser.cs index f0c3beca..f84442c1 100644 --- a/Penumbra/Import/Textures/TexFileParser.cs +++ b/Penumbra/Import/Textures/TexFileParser.cs @@ -101,7 +101,7 @@ public static class TexFileParser Height = (ushort)meta.Height, Width = (ushort)meta.Width, Depth = (ushort)Math.Max(meta.Depth, 1), - MipLevels = (byte)Math.Min(meta.MipLevels, 12), + MipLevels = (byte)Math.Min(meta.MipLevels, 13), Format = meta.Format.ToTexFormat(), Type = meta.Dimension switch { diff --git a/Penumbra/Import/Textures/Texture.cs b/Penumbra/Import/Textures/Texture.cs index ef8e16fc..aefe72b4 100644 --- a/Penumbra/Import/Textures/Texture.cs +++ b/Penumbra/Import/Textures/Texture.cs @@ -1,135 +1,61 @@ using System; -using System.Collections.Generic; -using System.IO; -using System.Numerics; -using Dalamud.Data; -using Dalamud.Interface; -using ImGuiNET; using ImGuiScene; -using Lumina.Data.Files; -using OtterGui; -using OtterGui.Raii; using OtterTex; -using Penumbra.Services; -using Penumbra.String.Classes; -using Penumbra.UI; -using Penumbra.UI.Classes; -using SixLabors.ImageSharp.PixelFormats; -using Image = SixLabors.ImageSharp.Image; namespace Penumbra.Import.Textures; +public enum TextureType +{ + Unknown, + Dds, + Tex, + Png, + Bitmap, +} + public sealed class Texture : IDisposable { - public enum FileType - { - Unknown, - Dds, - Tex, - Png, - Bitmap, - } - // Path to the file we tried to load. public string Path = string.Empty; + // Path for changing paths. + internal string? TmpPath; + // If the load failed, an exception is stored. public Exception? LoadError = null; // The pixels of the main image in RGBA order. // Empty if LoadError != null or Path is empty. - public byte[] RGBAPixels = Array.Empty(); + public byte[] RgbaPixels = Array.Empty(); // The ImGui wrapper to load the image. // null if LoadError != null or Path is empty. public TextureWrap? TextureWrap = null; // The base image in whatever format it has. - public object? BaseImage = null; + public BaseImage BaseImage; // Original File Type. - public FileType Type = FileType.Unknown; + public TextureType Type = TextureType.Unknown; // Whether the file is successfully loaded and drawable. public bool IsLoaded => TextureWrap != null; public DXGIFormat Format - => BaseImage switch - { - ScratchImage s => s.Meta.Format, - TexFile t => t.Header.Format.ToDXGI(), - _ => DXGIFormat.Unknown, - }; + => BaseImage.Format; public int MipMaps - => BaseImage switch - { - ScratchImage s => s.Meta.MipLevels, - TexFile t => t.Header.MipLevels, - _ => 1, - }; - - public void Draw(Vector2 size) - { - if (TextureWrap != null) - { - size = size.X < TextureWrap.Width - ? size with { Y = TextureWrap.Height * size.X / TextureWrap.Width } - : new Vector2(TextureWrap.Width, TextureWrap.Height); - - ImGui.Image(TextureWrap.ImGuiHandle, size); - DrawData(); - } - else if (LoadError != null) - { - ImGui.TextUnformatted("Could not load file:"); - ImGuiUtil.TextColored(Colors.RegexWarningBorder, LoadError.ToString()); - } - } - - public void DrawData() - { - using var table = ImRaii.Table("##data", 2, ImGuiTableFlags.SizingFixedFit); - ImGuiUtil.DrawTableColumn("Width"); - ImGuiUtil.DrawTableColumn(TextureWrap!.Width.ToString()); - ImGuiUtil.DrawTableColumn("Height"); - ImGuiUtil.DrawTableColumn(TextureWrap!.Height.ToString()); - ImGuiUtil.DrawTableColumn("File Type"); - ImGuiUtil.DrawTableColumn(Type.ToString()); - ImGuiUtil.DrawTableColumn("Bitmap Size"); - ImGuiUtil.DrawTableColumn($"{Functions.HumanReadableSize(RGBAPixels.Length)} ({RGBAPixels.Length} Bytes)"); - switch (BaseImage) - { - case ScratchImage s: - ImGuiUtil.DrawTableColumn("Format"); - ImGuiUtil.DrawTableColumn(s.Meta.Format.ToString()); - ImGuiUtil.DrawTableColumn("Mip Levels"); - ImGuiUtil.DrawTableColumn(s.Meta.MipLevels.ToString()); - ImGuiUtil.DrawTableColumn("Data Size"); - ImGuiUtil.DrawTableColumn($"{Functions.HumanReadableSize(s.Pixels.Length)} ({s.Pixels.Length} Bytes)"); - ImGuiUtil.DrawTableColumn("Number of Images"); - ImGuiUtil.DrawTableColumn(s.Images.Length.ToString()); - break; - case TexFile t: - ImGuiUtil.DrawTableColumn("Format"); - ImGuiUtil.DrawTableColumn(t.Header.Format.ToString()); - ImGuiUtil.DrawTableColumn("Mip Levels"); - ImGuiUtil.DrawTableColumn(t.Header.MipLevels.ToString()); - ImGuiUtil.DrawTableColumn("Data Size"); - ImGuiUtil.DrawTableColumn($"{Functions.HumanReadableSize(t.ImageData.Length)} ({t.ImageData.Length} Bytes)"); - break; - } - } + => BaseImage.MipMaps; private void Clean() { - RGBAPixels = Array.Empty(); + RgbaPixels = Array.Empty(); TextureWrap?.Dispose(); TextureWrap = null; - (BaseImage as IDisposable)?.Dispose(); - BaseImage = null; - Type = FileType.Unknown; + BaseImage.Dispose(); + BaseImage = new BaseImage(); + Type = TextureType.Unknown; Loaded?.Invoke(false); } @@ -138,9 +64,9 @@ public sealed class Texture : IDisposable public event Action? Loaded; - private void Load(DalamudServices dalamud, string path) + public void Load(TextureManager textures, string path) { - _tmpPath = null; + TmpPath = null; if (path == Path) return; @@ -151,13 +77,9 @@ public sealed class Texture : IDisposable try { - var _ = System.IO.Path.GetExtension(Path).ToLowerInvariant() switch - { - ".dds" => LoadDds(dalamud), - ".png" => LoadPng(dalamud), - ".tex" => LoadTex(dalamud), - _ => throw new Exception($"Extension {System.IO.Path.GetExtension(Path)} unknown."), - }; + (BaseImage, Type) = textures.Load(path); + (RgbaPixels, var width, var height) = BaseImage.GetPixelData(); + TextureWrap = textures.LoadTextureWrap(BaseImage, RgbaPixels); Loaded?.Invoke(true); } catch (Exception e) @@ -167,130 +89,10 @@ public sealed class Texture : IDisposable } } - public void Reload(DalamudServices dalamud) + public void Reload(TextureManager textures) { var path = Path; - Path = string.Empty; - Load(dalamud, path); - } - - private bool LoadDds(DalamudServices dalamud) - { - Type = FileType.Dds; - var scratch = ScratchImage.LoadDDS(Path); - BaseImage = scratch; - var rgba = scratch.GetRGBA(out var f).ThrowIfError(f); - RGBAPixels = rgba.Pixels[..(f.Meta.Width * f.Meta.Height * f.Meta.Format.BitsPerPixel() / 8)].ToArray(); - CreateTextureWrap(dalamud.UiBuilder, f.Meta.Width, f.Meta.Height); - return true; - } - - private bool LoadPng(DalamudServices dalamud) - { - Type = FileType.Png; - BaseImage = null; - using var stream = File.OpenRead(Path); - using var png = Image.Load(stream); - RGBAPixels = new byte[png.Height * png.Width * 4]; - png.CopyPixelDataTo(RGBAPixels); - CreateTextureWrap(dalamud.UiBuilder, png.Width, png.Height); - return true; - } - - private bool LoadTex(DalamudServices dalamud) - { - Type = FileType.Tex; - using var stream = OpenTexStream(dalamud.GameData); - var scratch = TexFileParser.Parse(stream); - BaseImage = scratch; - var rgba = scratch.GetRGBA(out var f).ThrowIfError(f); - RGBAPixels = rgba.Pixels[..(f.Meta.Width * f.Meta.Height * (f.Meta.Format.BitsPerPixel() / 8))].ToArray(); - CreateTextureWrap(dalamud.UiBuilder, scratch.Meta.Width, scratch.Meta.Height); - return true; - } - - private Stream OpenTexStream(DataManager gameData) - { - if (System.IO.Path.IsPathRooted(Path)) - return File.OpenRead(Path); - - var file = gameData.GetFile(Path); - return file != null ? new MemoryStream(file.Data) : throw new Exception($"Unable to obtain \"{Path}\" from game files."); - } - - private void CreateTextureWrap(UiBuilder builder, int width, int height) - => TextureWrap = builder.LoadImageRaw(RGBAPixels, width, height, 4); - - private string? _tmpPath; - - public void PathSelectBox(DalamudServices dalamud, string label, string tooltip, IEnumerable<(string, bool)> paths, int skipPrefix) - { - ImGui.SetNextItemWidth(-0.0001f); - var startPath = Path.Length > 0 ? Path : "Choose a modded texture from this mod here..."; - using var combo = ImRaii.Combo(label, startPath); - if (combo) - foreach (var ((path, game), idx) in paths.WithIndex()) - { - if (game) - { - if (!dalamud.GameData.FileExists(path)) - continue; - } - else if (!File.Exists(path)) - { - continue; - } - - using var id = ImRaii.PushId(idx); - using (var color = ImRaii.PushColor(ImGuiCol.Text, ColorId.FolderExpanded.Value(), game)) - { - var p = game ? $"--> {path}" : path[skipPrefix..]; - if (ImGui.Selectable(p, path == startPath) && path != startPath) - Load(dalamud, path); - } - - ImGuiUtil.HoverTooltip(game - ? "This is a game path and refers to an unmanipulated file from your game data." - : "This is a path to a modded file on your file system."); - } - - ImGuiUtil.HoverTooltip(tooltip); - } - - public void PathInputBox(DalamudServices dalamud, string label, string hint, string tooltip, string startPath, FileDialogService fileDialog, - string defaultModImportPath) - { - _tmpPath ??= Path; - using var spacing = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, - new Vector2(UiHelpers.ScaleX3, ImGui.GetStyle().ItemSpacing.Y)); - ImGui.SetNextItemWidth(-2 * ImGui.GetFrameHeight() - 7 * UiHelpers.Scale); - ImGui.InputTextWithHint(label, hint, ref _tmpPath, Utf8GamePath.MaxGamePathLength); - if (ImGui.IsItemDeactivatedAfterEdit()) - Load(dalamud, _tmpPath); - - ImGuiUtil.HoverTooltip(tooltip); - ImGui.SameLine(); - if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Folder.ToIconString(), new Vector2(ImGui.GetFrameHeight()), string.Empty, false, - true)) - { - if (defaultModImportPath.Length > 0) - startPath = defaultModImportPath; - - var texture = this; - - void UpdatePath(bool success, List paths) - { - if (success && paths.Count > 0) - texture.Load(dalamud, paths[0]); - } - - fileDialog.OpenFilePicker("Open Image...", "Textures{.png,.dds,.tex}", UpdatePath, 1, startPath, false); - } - - ImGui.SameLine(); - if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Recycle.ToIconString(), new Vector2(ImGui.GetFrameHeight()), - "Reload the currently selected path.", false, - true)) - Reload(dalamud); + Path = string.Empty; + Load(textures, path); } } diff --git a/Penumbra/Import/Textures/TextureDrawer.cs b/Penumbra/Import/Textures/TextureDrawer.cs new file mode 100644 index 00000000..b077f6fd --- /dev/null +++ b/Penumbra/Import/Textures/TextureDrawer.cs @@ -0,0 +1,139 @@ +using System.Collections.Generic; +using System.IO; +using System.Numerics; +using Dalamud.Interface; +using ImGuiNET; +using Lumina.Data.Files; +using OtterGui; +using OtterGui.Raii; +using OtterTex; +using Penumbra.String.Classes; +using Penumbra.UI; +using Penumbra.UI.Classes; + +namespace Penumbra.Import.Textures; + +public static class TextureDrawer +{ + public static void Draw(Texture texture, Vector2 size) + { + if (texture.TextureWrap != null) + { + size = size.X < texture.TextureWrap.Width + ? size with { Y = texture.TextureWrap.Height * size.X / texture.TextureWrap.Width } + : new Vector2(texture.TextureWrap.Width, texture.TextureWrap.Height); + + ImGui.Image(texture.TextureWrap.ImGuiHandle, size); + DrawData(texture); + } + else if (texture.LoadError != null) + { + ImGui.TextUnformatted("Could not load file:"); + ImGuiUtil.TextColored(Colors.RegexWarningBorder, texture.LoadError.ToString()); + } + } + + public static void PathSelectBox(TextureManager textures, Texture current, string label, string tooltip, IEnumerable<(string, bool)> paths, + int skipPrefix) + { + ImGui.SetNextItemWidth(-0.0001f); + var startPath = current.Path.Length > 0 ? current.Path : "Choose a modded texture from this mod here..."; + using var combo = ImRaii.Combo(label, startPath); + if (combo) + foreach (var ((path, game), idx) in paths.WithIndex()) + { + if (game) + { + if (!textures.GameFileExists(path)) + continue; + } + else if (!File.Exists(path)) + { + continue; + } + + using var id = ImRaii.PushId(idx); + using (var color = ImRaii.PushColor(ImGuiCol.Text, ColorId.FolderExpanded.Value(), game)) + { + var p = game ? $"--> {path}" : path[skipPrefix..]; + if (ImGui.Selectable(p, path == startPath) && path != startPath) + current.Load(textures, path); + } + + ImGuiUtil.HoverTooltip(game + ? "This is a game path and refers to an unmanipulated file from your game data." + : "This is a path to a modded file on your file system."); + } + + ImGuiUtil.HoverTooltip(tooltip); + } + + public static void PathInputBox(TextureManager textures, Texture current, ref string? tmpPath, string label, string hint, string tooltip, + string startPath, FileDialogService fileDialog, string defaultModImportPath) + { + tmpPath ??= current.Path; + using var spacing = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, + new Vector2(UiHelpers.ScaleX3, ImGui.GetStyle().ItemSpacing.Y)); + ImGui.SetNextItemWidth(-2 * ImGui.GetFrameHeight() - 7 * UiHelpers.Scale); + ImGui.InputTextWithHint(label, hint, ref tmpPath, Utf8GamePath.MaxGamePathLength); + if (ImGui.IsItemDeactivatedAfterEdit()) + current.Load(textures, tmpPath); + + ImGuiUtil.HoverTooltip(tooltip); + ImGui.SameLine(); + if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Folder.ToIconString(), new Vector2(ImGui.GetFrameHeight()), string.Empty, false, + true)) + { + if (defaultModImportPath.Length > 0) + startPath = defaultModImportPath; + + void UpdatePath(bool success, List paths) + { + if (success && paths.Count > 0) + current.Load(textures, paths[0]); + } + + fileDialog.OpenFilePicker("Open Image...", "Textures{.png,.dds,.tex}", UpdatePath, 1, startPath, false); + } + + ImGui.SameLine(); + if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Recycle.ToIconString(), new Vector2(ImGui.GetFrameHeight()), + "Reload the currently selected path.", false, + true)) + current.Reload(textures); + } + + private static void DrawData(Texture texture) + { + using var table = ImRaii.Table("##data", 2, ImGuiTableFlags.SizingFixedFit); + ImGuiUtil.DrawTableColumn("Width"); + ImGuiUtil.DrawTableColumn(texture.TextureWrap!.Width.ToString()); + ImGuiUtil.DrawTableColumn("Height"); + ImGuiUtil.DrawTableColumn(texture.TextureWrap!.Height.ToString()); + ImGuiUtil.DrawTableColumn("File Type"); + ImGuiUtil.DrawTableColumn(texture.Type.ToString()); + ImGuiUtil.DrawTableColumn("Bitmap Size"); + ImGuiUtil.DrawTableColumn($"{Functions.HumanReadableSize(texture.RgbaPixels.Length)} ({texture.RgbaPixels.Length} Bytes)"); + switch (texture.BaseImage.Image) + { + case ScratchImage s: + ImGuiUtil.DrawTableColumn("Format"); + ImGuiUtil.DrawTableColumn(s.Meta.Format.ToString()); + ImGuiUtil.DrawTableColumn("Mip Levels"); + ImGuiUtil.DrawTableColumn(s.Meta.MipLevels.ToString()); + ImGuiUtil.DrawTableColumn("Data Size"); + ImGuiUtil.DrawTableColumn($"{Functions.HumanReadableSize(s.Pixels.Length)} ({s.Pixels.Length} Bytes)"); + ImGuiUtil.DrawTableColumn("Number of Images"); + ImGuiUtil.DrawTableColumn(s.Images.Length.ToString()); + break; + case TexFile t: + ImGuiUtil.DrawTableColumn("Format"); + ImGuiUtil.DrawTableColumn(t.Header.Format.ToString()); + ImGuiUtil.DrawTableColumn("Mip Levels"); + ImGuiUtil.DrawTableColumn(t.Header.MipLevels.ToString()); + ImGuiUtil.DrawTableColumn("Data Size"); + ImGuiUtil.DrawTableColumn($"{Functions.HumanReadableSize(t.ImageData.Length)} ({t.ImageData.Length} Bytes)"); + break; + } + } +} diff --git a/Penumbra/Import/Textures/TextureImporter.cs b/Penumbra/Import/Textures/TextureImporter.cs deleted file mode 100644 index 74bef485..00000000 --- a/Penumbra/Import/Textures/TextureImporter.cs +++ /dev/null @@ -1,61 +0,0 @@ -using System; -using System.IO; -using Lumina.Data.Files; -using SixLabors.ImageSharp; -using SixLabors.ImageSharp.PixelFormats; - -namespace Penumbra.Import.Textures; - -public static class TextureImporter -{ - private static void WriteHeader( byte[] target, int width, int height ) - { - using var mem = new MemoryStream( target ); - using var bw = new BinaryWriter( mem ); - bw.Write( ( uint )TexFile.Attribute.TextureType2D ); - bw.Write( ( uint )TexFile.TextureFormat.B8G8R8A8 ); - bw.Write( ( ushort )width ); - bw.Write( ( ushort )height ); - bw.Write( ( ushort )1 ); - bw.Write( ( ushort )1 ); - bw.Write( 0 ); - bw.Write( 1 ); - bw.Write( 2 ); - bw.Write( 80 ); - for( var i = 1; i < 13; ++i ) - { - bw.Write( 0 ); - } - } - - public static bool RgbaBytesToTex( byte[] rgba, int width, int height, out byte[] texData ) - { - texData = Array.Empty< byte >(); - if( rgba.Length != width * height * 4 ) - { - return false; - } - - texData = new byte[80 + width * height * 4]; - WriteHeader( texData, width, height ); - rgba.CopyTo( texData.AsSpan( 80 ) ); - for( var i = 80; i < texData.Length; i += 4 ) - (texData[ i ], texData[i + 2]) = (texData[ i + 2], texData[i]); - return true; - } - - public static bool PngToTex( string inputFile, out byte[] texData ) - { - using var file = File.OpenRead( inputFile ); - var image = Image.Load< Bgra32 >( file ); - - var buffer = new byte[80 + image.Height * image.Width * 4]; - WriteHeader( buffer, image.Width, image.Height ); - - var span = new Span< byte >( buffer, 80, buffer.Length - 80 ); - image.CopyPixelDataTo( span ); - - texData = buffer; - return true; - } -} \ No newline at end of file diff --git a/Penumbra/Import/Textures/TextureManager.cs b/Penumbra/Import/Textures/TextureManager.cs new file mode 100644 index 00000000..9ac503df --- /dev/null +++ b/Penumbra/Import/Textures/TextureManager.cs @@ -0,0 +1,438 @@ +using System; +using System.IO; +using System.Linq; +using System.Numerics; +using System.Threading; +using Dalamud.Data; +using Dalamud.Interface; +using ImGuiScene; +using Lumina.Data.Files; +using OtterGui.Log; +using OtterGui.Tasks; +using OtterTex; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.PixelFormats; +using Image = SixLabors.ImageSharp.Image; + +namespace Penumbra.Import.Textures; + +public sealed class TextureManager : AsyncTaskManager +{ + private readonly UiBuilder _uiBuilder; + private readonly DataManager _gameData; + + public TextureManager(Logger logger, UiBuilder uiBuilder, DataManager gameData) + : base(logger) + { + _uiBuilder = uiBuilder; + _gameData = gameData; + } + + public Guid 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) + => Enqueue(new SavePngAction(this, image, path, rgba, width, height)); + + public Guid 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, + int width = 0, int height = 0) + => Enqueue(new SaveAsAction(this, type, mipMaps, asTex, image, path, rgba, width, height)); + + 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", + }; + } + + private class SavePngAction : IAction + { + private readonly TextureManager _textures; + private readonly string _outputPath; + private readonly ImageInputData _input; + + public SavePngAction(TextureManager textures, string input, string output) + { + _textures = textures; + _input = new ImageInputData(input); + _outputPath = output; + } + + public SavePngAction(TextureManager textures, BaseImage image, string path, byte[]? rgba = null, int width = 0, int height = 0) + { + _textures = textures; + _input = new ImageInputData(image, rgba, width, height); + _outputPath = path; + } + + public void Execute(CancellationToken cancel) + { + _textures.Logger.Information($"[{nameof(TextureManager)}] Saving {_input} as .png to {_outputPath}..."); + var (image, rgba, width, height) = _input.GetData(_textures); + cancel.ThrowIfCancellationRequested(); + Image? png = null; + if (image.Type is TextureType.Unknown) + { + if (rgba != null && width > 0 && height > 0) + png = ConvertToPng(rgba, width, height).AsPng!; + } + else + { + png = ConvertToPng(image, cancel, rgba).AsPng!; + } + + cancel.ThrowIfCancellationRequested(); + png?.SaveAsync(_outputPath, cancel).Wait(cancel); + } + + public bool Equals(IAction? other) + { + if (other is not SavePngAction rhs) + return false; + + return string.Equals(_outputPath, rhs._outputPath, StringComparison.OrdinalIgnoreCase) && _input.Equals(rhs._input); + } + } + + private class SaveAsAction : IAction + { + private readonly TextureManager _textures; + private readonly string _outputPath; + private readonly ImageInputData _input; + private readonly CombinedTexture.TextureSaveType _type; + private readonly bool _mipMaps; + private readonly bool _asTex; + + public SaveAsAction(TextureManager textures, CombinedTexture.TextureSaveType type, bool mipMaps, bool asTex, string input, + string output) + { + _textures = textures; + _input = new ImageInputData(input); + _outputPath = output; + _type = type; + _mipMaps = mipMaps; + _asTex = asTex; + } + + public SaveAsAction(TextureManager textures, CombinedTexture.TextureSaveType type, bool mipMaps, bool asTex, BaseImage image, + string path, + byte[]? rgba = null, int width = 0, int height = 0) + { + _textures = textures; + _input = new ImageInputData(image, rgba, width, height); + _outputPath = path; + _type = type; + _mipMaps = mipMaps; + _asTex = asTex; + } + + public void Execute(CancellationToken cancel) + { + _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) + { + if (rgba != null && width > 0 && height > 0) + image = ConvertToDds(rgba, width, height); + else + return; + } + + var dds = _type switch + { + CombinedTexture.TextureSaveType.AsIs when image.Type is TextureType.Png => ConvertToRgbaDds(image, _mipMaps, cancel, rgba, + width, height), + CombinedTexture.TextureSaveType.AsIs when image.Type is TextureType.Dds => AddMipMaps(image.AsDds!, _mipMaps), + CombinedTexture.TextureSaveType.Bitmap => ConvertToRgbaDds(image, _mipMaps, cancel, rgba, width, height), + CombinedTexture.TextureSaveType.BC3 => ConvertToCompressedDds(image, _mipMaps, false, cancel, rgba, width, height), + CombinedTexture.TextureSaveType.BC7 => + ConvertToCompressedDds(image, _mipMaps, true, cancel, rgba, width, height), + _ => throw new Exception("Wrong save type."), + }; + + cancel.ThrowIfCancellationRequested(); + if (_asTex) + SaveTex(_outputPath, dds.AsDds!); + else + dds.AsDds!.SaveDDS(_outputPath); + } + + public bool Equals(IAction? other) + { + if (other is not SaveAsAction rhs) + return false; + + return _type == rhs._type + && _mipMaps == rhs._mipMaps + && _asTex == rhs._asTex + && string.Equals(_outputPath, rhs._outputPath, StringComparison.OrdinalIgnoreCase) + && _input.Equals(rhs._input); + } + } + + /// Load a texture wrap for a given image. + public TextureWrap LoadTextureWrap(BaseImage image, byte[]? rgba = null, int width = 0, int height = 0) + { + (rgba, width, height) = GetData(image, rgba, width, height); + return LoadTextureWrap(rgba, width, height); + } + + /// Load a texture wrap for a given image. + public TextureWrap LoadTextureWrap(byte[] rgba, int width, int height) + => _uiBuilder.LoadImageRaw(rgba, width, height, 4); + + /// Load any supported file from game data or drive depending on extension and if the path is rooted. + public (BaseImage, TextureType) Load(string path) + => Path.GetExtension(path).ToLowerInvariant() switch + { + ".dds" => (LoadDds(path), TextureType.Dds), + ".png" => (LoadPng(path), TextureType.Png), + ".tex" => (LoadTex(path), TextureType.Tex), + _ => throw new Exception($"Extension {Path.GetExtension(path)} unknown."), + }; + + /// Load a .tex file from game data or drive depending on if the path is rooted. + public BaseImage LoadTex(string path) + { + using var stream = OpenTexStream(path); + return TexFileParser.Parse(stream); + } + + /// Load a .dds file from drive using OtterTex. + public BaseImage LoadDds(string path) + => ScratchImage.LoadDDS(path); + + /// Load a .png file from drive using ImageSharp. + public BaseImage LoadPng(string path) + { + using var stream = File.OpenRead(path); + return Image.Load(stream); + } + + /// Convert an existing image to .png. Does not create a deep copy of an existing .png and just returns the existing one. + public static BaseImage ConvertToPng(BaseImage input, CancellationToken cancel, byte[]? rgba = null, int width = 0, int height = 0) + { + switch (input.Type) + { + case TextureType.Png: return input; + case TextureType.Dds: + { + (rgba, width, height) = GetData(input, rgba, width, height); + cancel.ThrowIfCancellationRequested(); + return ConvertToPng(rgba, width, height); + } + default: return new BaseImage(); + } + } + + /// Convert an existing image to a RGBA32 .dds. Does not create a deep copy of an existing RGBA32 dds and just returns the existing one. + public static BaseImage ConvertToRgbaDds(BaseImage input, bool mipMaps, CancellationToken cancel, byte[]? rgba = null, int width = 0, + int height = 0) + { + switch (input.Type) + { + case TextureType.Png: + { + (rgba, width, height) = GetData(input, rgba, width, height); + cancel.ThrowIfCancellationRequested(); + var dds = ConvertToDds(rgba, width, height).AsDds!; + cancel.ThrowIfCancellationRequested(); + return AddMipMaps(dds, mipMaps); + } + case TextureType.Dds: + { + var scratch = input.AsDds!; + if (rgba == null) + return CreateUncompressed(scratch, mipMaps, cancel); + + (rgba, width, height) = GetData(input, rgba, width, height); + cancel.ThrowIfCancellationRequested(); + var dds = ConvertToDds(rgba, width, height).AsDds!; + cancel.ThrowIfCancellationRequested(); + return AddMipMaps(dds, mipMaps); + } + default: return new BaseImage(); + } + } + + /// Convert an existing image to a block compressed .dds. Does not create a deep copy of an existing dds of the correct format and just returns the existing one. + public static BaseImage ConvertToCompressedDds(BaseImage input, bool mipMaps, bool bc7, CancellationToken cancel, byte[]? rgba = null, + int width = 0, int height = 0) + { + switch (input.Type) + { + case TextureType.Png: + { + (rgba, width, height) = GetData(input, rgba, width, height); + cancel.ThrowIfCancellationRequested(); + var dds = ConvertToDds(rgba, width, height).AsDds!; + cancel.ThrowIfCancellationRequested(); + return CreateCompressed(dds, mipMaps, bc7, cancel); + } + case TextureType.Dds: + { + var scratch = input.AsDds!; + return CreateCompressed(scratch, mipMaps, bc7, cancel); + } + default: return new BaseImage(); + } + } + + public static BaseImage ConvertToPng(byte[] rgba, int width, int height) + => Image.LoadPixelData(rgba, width, height); + + public static BaseImage ConvertToDds(byte[] rgba, int width, int height) + { + var scratch = ScratchImage.FromRGBA(rgba, width, height, out var i).ThrowIfError(i); + return scratch.Convert(DXGIFormat.B8G8R8A8UNorm); + } + + public bool GameFileExists(string path) + => _gameData.FileExists(path); + + /// Add up to 13 mip maps to the input if mip maps is true, otherwise return input. + public static ScratchImage AddMipMaps(ScratchImage input, bool mipMaps) + { + var numMips = mipMaps ? Math.Min(13, 1 + BitOperations.Log2((uint)Math.Max(input.Meta.Width, input.Meta.Height))) : 1; + if (numMips == input.Meta.MipLevels) + return input; + + var ec = input.GenerateMipMaps(out var ret, numMips, + (Dalamud.Utility.Util.IsLinux() ? FilterFlags.ForceNonWIC : 0) | FilterFlags.SeparateAlpha); + if (ec != ErrorCode.Ok) + throw new Exception($"Could not create the requested {numMips} mip maps, maybe retry with the top-right checkbox unchecked:\n{ec}"); + + return ret; + } + + /// Create an uncompressed .dds (optionally with mip maps) from the input. Returns input (+ mipmaps) if it is already uncompressed. + public static ScratchImage CreateUncompressed(ScratchImage input, bool mipMaps, CancellationToken cancel) + { + if (input.Meta.Format == DXGIFormat.B8G8R8A8UNorm) + return AddMipMaps(input, mipMaps); + + input = input.Meta.Format.IsCompressed() + ? input.Decompress(DXGIFormat.B8G8R8A8UNorm) + : input.Convert(DXGIFormat.B8G8R8A8UNorm); + cancel.ThrowIfCancellationRequested(); + return AddMipMaps(input, mipMaps); + } + + /// Create a BC3 or BC7 block-compressed .dds from the input (optionally with mipmaps). Returns input (+ mipmaps) if it is already the correct format. + public static ScratchImage CreateCompressed(ScratchImage input, bool mipMaps, bool bc7, CancellationToken cancel) + { + var format = bc7 ? DXGIFormat.BC7UNorm : DXGIFormat.BC3UNorm; + if (input.Meta.Format == format) + return input; + + if (input.Meta.Format.IsCompressed()) + { + input = input.Decompress(DXGIFormat.B8G8R8A8UNorm); + cancel.ThrowIfCancellationRequested(); + } + + input = AddMipMaps(input, mipMaps); + cancel.ThrowIfCancellationRequested(); + return input.Compress(format, CompressFlags.BC7Quick | CompressFlags.Parallel); + } + + + /// Load a tex file either from game data if the path is not rooted, or from drive if it is rooted. + private Stream OpenTexStream(string path) + { + if (Path.IsPathRooted(path)) + return File.OpenRead(path); + + var file = _gameData.GetFile(path); + return file != null ? new MemoryStream(file.Data) : throw new Exception($"Unable to obtain \"{path}\" from game files."); + } + + /// Obtain the checked rgba data, width and height for an image. + private static (byte[], int, int) GetData(BaseImage input, byte[]? rgba, int width, int height) + { + if (rgba == null) + return input.GetPixelData(); + + if (width == 0 || height == 0) + (width, height) = input.Dimensions; + return width * height * 4 != rgba.Length + ? input.GetPixelData() + : (rgba, width, height); + } + + /// Save a .dds file as .tex file with appropriately changed header. + public static void SaveTex(string path, ScratchImage input) + { + var header = input.ToTexHeader(); + if (header.Format == TexFile.TextureFormat.Unknown) + throw new Exception($"Could not save tex file with format {input.Meta.Format}, not convertible to a valid .tex format."); + + using var stream = File.Open(path, File.Exists(path) ? FileMode.Truncate : FileMode.CreateNew); + using var w = new BinaryWriter(stream); + header.Write(w); + w.Write(input.Pixels); + } +} diff --git a/Penumbra/Mods/Editor/ModBackup.cs b/Penumbra/Mods/Editor/ModBackup.cs index cbb98a38..eb15de87 100644 --- a/Penumbra/Mods/Editor/ModBackup.cs +++ b/Penumbra/Mods/Editor/ModBackup.cs @@ -3,7 +3,7 @@ using System.Collections.Generic; using System.IO; using System.IO.Compression; using System.Threading.Tasks; -using OtterGui; +using OtterGui.Tasks; using Penumbra.Mods.Manager; namespace Penumbra.Mods.Editor; diff --git a/Penumbra/Mods/Editor/ModNormalizer.cs b/Penumbra/Mods/Editor/ModNormalizer.cs index 1726eab2..62d87815 100644 --- a/Penumbra/Mods/Editor/ModNormalizer.cs +++ b/Penumbra/Mods/Editor/ModNormalizer.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Threading.Tasks; using Dalamud.Interface.Internal.Notifications; using OtterGui; +using OtterGui.Tasks; using Penumbra.Mods.Manager; using Penumbra.String.Classes; diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index fbbaab51..b291e392 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -22,7 +22,8 @@ using Penumbra.Collections.Manager; using Penumbra.UI.Tabs; using ChangedItemClick = Penumbra.Communication.ChangedItemClick; using ChangedItemHover = Penumbra.Communication.ChangedItemHover; - +using OtterGui.Tasks; + namespace Penumbra; public class Penumbra : IDalamudPlugin diff --git a/Penumbra/Services/ServiceManager.cs b/Penumbra/Services/ServiceManager.cs index 782d40a0..f0864b97 100644 --- a/Penumbra/Services/ServiceManager.cs +++ b/Penumbra/Services/ServiceManager.cs @@ -7,6 +7,7 @@ using Penumbra.Collections.Cache; using Penumbra.Collections.Manager; using Penumbra.GameData; using Penumbra.GameData.Data; +using Penumbra.Import.Textures; using Penumbra.Interop.PathResolving; using Penumbra.Interop.ResourceLoading; using Penumbra.Interop.ResourceTree; @@ -173,7 +174,8 @@ public static class ServiceManager .AddSingleton() .AddSingleton() .AddSingleton() - .AddSingleton(); + .AddSingleton() + .AddSingleton(); private static IServiceCollection AddApi(this IServiceCollection services) => services.AddSingleton() diff --git a/Penumbra/Services/ServiceWrapper.cs b/Penumbra/Services/ServiceWrapper.cs index 783acc49..ca1e3624 100644 --- a/Penumbra/Services/ServiceWrapper.cs +++ b/Penumbra/Services/ServiceWrapper.cs @@ -1,6 +1,6 @@ using System; using System.Threading.Tasks; -using OtterGui; +using OtterGui.Tasks; using Penumbra.Util; namespace Penumbra.Services; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs index f96ab4da..6fd7b130 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs @@ -5,6 +5,7 @@ using System.Numerics; using ImGuiNET; using OtterGui; using OtterGui.Raii; +using OtterGui.Tasks; using OtterTex; using Penumbra.Import.Textures; @@ -12,13 +13,14 @@ namespace Penumbra.UI.AdvancedWindow; public partial class ModEditWindow { + private readonly TextureManager _textures; + private readonly Texture _left = new(); private readonly Texture _right = new(); private readonly CombinedTexture _center; private bool _overlayCollapsed = true; - - private bool _addMipMaps = true; + private bool _addMipMaps = true; private int _currentSaveAs; private static readonly (string, string)[] SaveAsStrings = @@ -42,13 +44,13 @@ public partial class ModEditWindow ImGuiUtil.DrawTextButton(label, new Vector2(-1, 0), ImGui.GetColorU32(ImGuiCol.FrameBg)); ImGui.NewLine(); - tex.PathInputBox(_dalamud, "##input", "Import Image...", "Can import game paths as well as your own files.", _mod!.ModPath.FullName, - _fileDialog, _config.DefaultModImportPath); + TextureDrawer.PathInputBox(_textures, tex, ref tex.TmpPath, "##input", "Import Image...", + "Can import game paths as well as your own files.", _mod!.ModPath.FullName, _fileDialog, _config.DefaultModImportPath); var files = _editor.Files.Tex.SelectMany(f => f.SubModUsage.Select(p => (p.Item2.ToString(), true)) .Prepend((f.File.FullName, false))); - tex.PathSelectBox(_dalamud, "##combo", - "Select the textures included in this mod on your drive or the ones they replace from the game files.", - files, _mod.ModPath.FullName.Length + 1); + TextureDrawer.PathSelectBox(_textures, tex, "##combo", + "Select the textures included in this mod on your drive or the ones they replace from the game files.", files, + _mod.ModPath.FullName.Length + 1); if (tex == _left) _center.DrawMatrixInputLeft(size.X); @@ -58,7 +60,7 @@ public partial class ModEditWindow ImGui.NewLine(); using var child2 = ImRaii.Child("image"); if (child2) - tex.Draw(imageSize); + TextureDrawer.Draw(tex, imageSize); } private void SaveAsCombo() @@ -105,7 +107,7 @@ public partial class ModEditWindow _fileDialog.OpenSavePicker("Save Texture as TEX...", ".tex", fileName, ".tex", (a, b) => { if (a) - _center.SaveAsTex(b, (CombinedTexture.TextureSaveType)_currentSaveAs, _addMipMaps); + _center.SaveAsTex(_textures, b, (CombinedTexture.TextureSaveType)_currentSaveAs, _addMipMaps); }, _mod!.ModPath.FullName, _forceTextureStartPath); _forceTextureStartPath = false; } @@ -116,7 +118,7 @@ public partial class ModEditWindow _fileDialog.OpenSavePicker("Save Texture as DDS...", ".dds", fileName, ".dds", (a, b) => { if (a) - _center.SaveAsDds(b, (CombinedTexture.TextureSaveType)_currentSaveAs, _addMipMaps); + _center.SaveAsDds(_textures, b, (CombinedTexture.TextureSaveType)_currentSaveAs, _addMipMaps); }, _mod!.ModPath.FullName, _forceTextureStartPath); _forceTextureStartPath = false; } @@ -129,20 +131,20 @@ public partial class ModEditWindow _fileDialog.OpenSavePicker("Save Texture as PNG...", ".png", fileName, ".png", (a, b) => { if (a) - _center.SaveAsPng(b); + _center.SaveAsPng(_textures, b); }, _mod!.ModPath.FullName, _forceTextureStartPath); _forceTextureStartPath = false; } - if (_left.Type is Texture.FileType.Tex && _center.IsLeftCopy) + if (_left.Type is TextureType.Tex && _center.IsLeftCopy) { var buttonSize = new Vector2((ImGui.GetContentRegionAvail().X - ImGui.GetStyle().ItemSpacing.X * 2) / 3, 0); if (ImGuiUtil.DrawDisabledButton("Convert to BC7", buttonSize, "This converts the texture to BC7 format in place. This is not revertible.", _left.Format is DXGIFormat.BC7Typeless or DXGIFormat.BC7UNorm or DXGIFormat.BC7UNormSRGB)) { - _center.SaveAsTex(_left.Path, CombinedTexture.TextureSaveType.BC7, _left.MipMaps > 1); - _left.Reload(_dalamud); + _center.SaveAsTex(_textures, _left.Path, CombinedTexture.TextureSaveType.BC7, _left.MipMaps > 1); + ReloadConvertedSubscribe(_left.Path, _center.SaveGuid); } ImGui.SameLine(); @@ -150,8 +152,8 @@ public partial class ModEditWindow "This converts the texture to BC3 format in place. This is not revertible.", _left.Format is DXGIFormat.BC3Typeless or DXGIFormat.BC3UNorm or DXGIFormat.BC3UNormSRGB)) { - _center.SaveAsTex(_left.Path, CombinedTexture.TextureSaveType.BC3, _left.MipMaps > 1); - _left.Reload(_dalamud); + _center.SaveAsTex(_textures, _left.Path, CombinedTexture.TextureSaveType.BC3, _left.MipMaps > 1); + ReloadConvertedSubscribe(_left.Path, _center.SaveGuid); } ImGui.SameLine(); @@ -159,27 +161,55 @@ public partial class ModEditWindow "This converts the texture to RGBA format in place. This is not revertible.", _left.Format is DXGIFormat.B8G8R8A8UNorm or DXGIFormat.B8G8R8A8Typeless or DXGIFormat.B8G8R8A8UNormSRGB)) { - _center.SaveAsTex(_left.Path, CombinedTexture.TextureSaveType.Bitmap, _left.MipMaps > 1); - _left.Reload(_dalamud); + _center.SaveAsTex(_textures, _left.Path, CombinedTexture.TextureSaveType.Bitmap, _left.MipMaps > 1); + ReloadConvertedSubscribe(_left.Path, _center.SaveGuid); } } else { ImGui.NewLine(); } + ImGui.NewLine(); } - if (_center.SaveException != null) + if (_center.SaveGuid != Guid.Empty) { - ImGui.TextUnformatted("Could not save file:"); - using var color = ImRaii.PushColor(ImGuiCol.Text, 0xFF0000FF); - ImGuiUtil.TextWrapped(_center.SaveException.ToString()); + var state = _textures.GetState(_center.SaveGuid, out var saveException, out _, out _); + if (saveException != null) + { + 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..."); + } } using var child2 = ImRaii.Child("image"); if (child2) - _center.Draw(_dalamud.UiBuilder, imageSize); + _center.Draw(_textures, imageSize); + } + + + private void ReloadConvertedSubscribe(string path, Guid guid) + { + void Reload(Guid eventGuid, ActionState state, Exception? ex) + { + if (guid != eventGuid) + return; + + if (_left.Path != path) + return; + + if (state is ActionState.Succeeded) + _dalamud.Framework.RunOnFrameworkThread(() => _left.Reload(_textures)); + _textures.Finished -= Reload; + } + + _textures.Finished += Reload; } private Vector2 GetChildWidth() diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs index b6051136..93d28b85 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs @@ -521,7 +521,7 @@ public partial class ModEditWindow : Window, IDisposable public ModEditWindow(PerformanceTracker performance, FileDialogService fileDialog, ItemSwapTab itemSwapTab, DataManager gameData, Configuration config, ModEditor editor, ResourceTreeFactory resourceTreeFactory, MetaFileManager metaFileManager, StainService stainService, ActiveCollections activeCollections, UiBuilder uiBuilder, DalamudServices dalamud, ModMergeTab modMergeTab, - CommunicatorService communicator) + CommunicatorService communicator, TextureManager textures) : base(WindowBaseLabel) { _performance = performance; @@ -534,6 +534,7 @@ public partial class ModEditWindow : Window, IDisposable _dalamud = dalamud; _modMergeTab = modMergeTab; _communicator = communicator; + _textures = textures; _fileDialog = fileDialog; _materialTab = new FileEditor(this, gameData, config, _fileDialog, "Materials", ".mtrl", () => _editor.Files.Mtrl, DrawMaterialPanel, () => _mod?.ModPath.FullName ?? string.Empty,