From 42b874413df85787af1b2acf8834b1dc6caa5153 Mon Sep 17 00:00:00 2001 From: Exter-N Date: Fri, 25 Aug 2023 06:28:09 +0200 Subject: [PATCH 01/15] Add a few texture manipulation tools. --- OtterGui | 2 +- .../Textures/CombinedTexture.Manipulation.cs | 319 ++++++++++++++++-- Penumbra/Import/Textures/CombinedTexture.cs | 69 ++-- .../AdvancedWindow/ModEditWindow.Textures.cs | 100 +++--- 4 files changed, 381 insertions(+), 109 deletions(-) diff --git a/OtterGui b/OtterGui index 863d08bd..c8394607 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 863d08bd83381bb7fe4a8d5c514f0ba55379336f +Subproject commit c8394607addd29cb7f8ae3257f635a4486c40a63 diff --git a/Penumbra/Import/Textures/CombinedTexture.Manipulation.cs b/Penumbra/Import/Textures/CombinedTexture.Manipulation.cs index a32b9578..f8608071 100644 --- a/Penumbra/Import/Textures/CombinedTexture.Manipulation.cs +++ b/Penumbra/Import/Textures/CombinedTexture.Manipulation.cs @@ -6,23 +6,110 @@ using ImGuiNET; using OtterGui.Raii; using OtterGui; using SixLabors.ImageSharp.PixelFormats; +using Dalamud.Interface; +using Penumbra.UI; namespace Penumbra.Import.Textures; public partial class CombinedTexture { + private enum CombineOp + { + LeftCopy = -3, + RightCopy = -2, + Invalid = -1, + Over = 0, + Under = 1, + LeftMultiply = 2, + RightMultiply = 3, + CopyChannels = 4, + } + + [Flags] + private enum Channels + { + Red = 1, + Green = 2, + Blue = 4, + Alpha = 8, + } + private Matrix4x4 _multiplierLeft = Matrix4x4.Identity; + private Vector4 _constantLeft = Vector4.Zero; private Matrix4x4 _multiplierRight = Matrix4x4.Identity; - private bool _invertLeft = false; - private bool _invertRight = false; + private Vector4 _constantRight = Vector4.Zero; private int _offsetX = 0; - private int _offsetY = 0; + private int _offsetY = 0; + private CombineOp _combineOp = CombineOp.Over; + private Channels _copyChannels = Channels.Red | Channels.Green | Channels.Blue | Channels.Alpha; + + private static readonly IReadOnlyList CombineOpLabels = new string[] + { + "Overlay over Input", + "Input over Overlay", + "Ignore Overlay", + "Replace Input", + "Copy Channels", + }; + + private static readonly IReadOnlyList CombineOpTooltips = new string[] + { + "Standard composition.\nApply the overlay over the input.", + "Standard composition, reversed.\nApply the input over the overlay.", + "Use only the input, and ignore the overlay.", + "Completely replace the input with the overlay.", + "Replace some input channels with those from the overlay.\nUseful for Multi maps.", + }; + + private const float OneThird = 1.0f / 3.0f; + private const float RWeight = 0.2126f; + private const float GWeight = 0.7152f; + private const float BWeight = 0.0722f; + + private static readonly IReadOnlyList<(string Label, Matrix4x4 Multiplier, Vector4 Constant)> PredefinedColorTransforms = new (string, Matrix4x4, Vector4)[] + { + ("No Transform (Identity)", Matrix4x4.Identity, Vector4.Zero), + ("Grayscale (Average)", new Matrix4x4(OneThird, OneThird, OneThird, 0.0f, OneThird, OneThird, OneThird, 0.0f, OneThird, OneThird, OneThird, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f), Vector4.Zero), + ("Grayscale (Weighted)", new Matrix4x4(RWeight, RWeight, RWeight, 0.0f, GWeight, GWeight, GWeight, 0.0f, BWeight, BWeight, BWeight, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f), Vector4.Zero), + ("Grayscale (Average) to Alpha", new Matrix4x4(OneThird, OneThird, OneThird, OneThird, OneThird, OneThird, OneThird, OneThird, OneThird, OneThird, OneThird, OneThird, 0.0f, 0.0f, 0.0f, 0.0f), Vector4.Zero), + ("Grayscale (Weighted) to Alpha", new Matrix4x4(RWeight, RWeight, RWeight, RWeight, GWeight, GWeight, GWeight, GWeight, BWeight, BWeight, BWeight, BWeight, 0.0f, 0.0f, 0.0f, 0.0f), Vector4.Zero), + ("Extract Red", new Matrix4x4(1.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f), Vector4.UnitW), + ("Extract Green", new Matrix4x4(0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f), Vector4.UnitW), + ("Extract Blue", new Matrix4x4(0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f), Vector4.UnitW), + ("Extract Alpha", new Matrix4x4(0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f, 0.0f), Vector4.UnitW), + }; + + private CombineOp GetActualCombineOp() + { + var combineOp = (_left.IsLoaded, _right.IsLoaded) switch + { + (true, true) => _combineOp, + (true, false) => CombineOp.LeftMultiply, + (false, true) => CombineOp.RightMultiply, + (false, false) => CombineOp.Invalid, + }; + + if (combineOp == CombineOp.CopyChannels) + { + if (_copyChannels == 0) + combineOp = CombineOp.LeftMultiply; + else if (_copyChannels == (Channels.Red | Channels.Green | Channels.Blue | Channels.Alpha)) + combineOp = CombineOp.RightMultiply; + } + + return combineOp switch + { + CombineOp.LeftMultiply => (_multiplierLeft.IsIdentity && _constantLeft == Vector4.Zero) ? CombineOp.LeftCopy : CombineOp.LeftMultiply, + CombineOp.RightMultiply => (_multiplierRight.IsIdentity && _constantRight == Vector4.Zero) ? CombineOp.RightCopy : CombineOp.RightMultiply, + _ => combineOp, + }; + } private Vector4 DataLeft( int offset ) - => CappedVector( _left.RgbaPixels, offset, _multiplierLeft, _invertLeft ); + => CappedVector( _left.RgbaPixels, offset, _multiplierLeft, _constantLeft ); private Vector4 DataRight( int offset ) - => CappedVector( _right.RgbaPixels, offset, _multiplierRight, _invertRight ); + => CappedVector( _right.RgbaPixels, offset, _multiplierRight, _constantRight ); private Vector4 DataRight( int x, int y ) { @@ -34,7 +121,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, _constantRight ); } private void AddPixelsMultiplied( int y, ParallelLoopState _ ) @@ -55,6 +142,43 @@ public partial class CombinedTexture } } + private void ReverseAddPixelsMultiplied( int y, ParallelLoopState _ ) + { + for( var x = 0; x < _left.TextureWrap!.Width; ++x ) + { + var offset = ( _left.TextureWrap!.Width * y + x ) * 4; + var left = DataLeft( offset ); + var right = DataRight( x, y ); + var alpha = left.W + right.W * ( 1 - left.W ); + var rgba = alpha == 0 + ? new Rgba32() + : new Rgba32( ( ( left * left.W + right * right.W * ( 1 - left.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; + } + } + + private void ChannelMergePixelsMultiplied( int y, ParallelLoopState _ ) + { + var channels = _copyChannels; + for( var x = 0; x < _left.TextureWrap!.Width; ++x ) + { + var offset = ( _left.TextureWrap!.Width * y + x ) * 4; + var left = DataLeft( offset ); + var right = DataRight( x, y ); + var rgba = new Rgba32( ( channels & Channels.Red ) != 0 ? right.X : left.X, + ( channels & Channels.Green ) != 0 ? right.Y : left.Y, + ( channels & Channels.Blue ) != 0 ? right.Z : left.Z, + ( channels & Channels.Alpha ) != 0 ? right.W : left.W ); + _centerStorage.RgbaPixels[ offset ] = rgba.R; + _centerStorage.RgbaPixels[ offset + 1 ] = rgba.G; + _centerStorage.RgbaPixels[ offset + 2 ] = rgba.B; + _centerStorage.RgbaPixels[ offset + 3 ] = rgba.A; + } + } + private void MultiplyPixelsLeft( int y, ParallelLoopState _ ) { for( var x = 0; x < _left.TextureWrap!.Width; ++x ) @@ -74,8 +198,8 @@ public partial class CombinedTexture for( var x = 0; x < _right.TextureWrap!.Width; ++x ) { var offset = ( _right.TextureWrap!.Width * y + x ) * 4; - var left = DataRight( offset ); - var rgba = new Rgba32( left ); + var right = DataRight( offset ); + var rgba = new Rgba32( right ); _centerStorage.RgbaPixels[ offset ] = rgba.R; _centerStorage.RgbaPixels[ offset + 1 ] = rgba.G; _centerStorage.RgbaPixels[ offset + 2 ] = rgba.B; @@ -86,23 +210,25 @@ public partial class CombinedTexture private (int Width, int Height) CombineImage() { - var (width, height) = _left.IsLoaded + var combineOp = GetActualCombineOp(); + var (width, height) = combineOp is not CombineOp.Invalid or CombineOp.RightCopy or CombineOp.RightMultiply ? ( _left.TextureWrap!.Width, _left.TextureWrap!.Height ) : ( _right.TextureWrap!.Width, _right.TextureWrap!.Height ); _centerStorage.RgbaPixels = new byte[width * height * 4]; _centerStorage.Type = TextureType.Bitmap; - if( _left.IsLoaded ) + Parallel.For( 0, height, combineOp switch { - Parallel.For( 0, height, _right.IsLoaded ? AddPixelsMultiplied : MultiplyPixelsLeft ); - } - else - { - Parallel.For( 0, height, MultiplyPixelsRight ); - } + CombineOp.Over => AddPixelsMultiplied, + CombineOp.Under => ReverseAddPixelsMultiplied, + CombineOp.LeftMultiply => MultiplyPixelsLeft, + CombineOp.RightMultiply => MultiplyPixelsRight, + CombineOp.CopyChannels => ChannelMergePixelsMultiplied, + _ => throw new InvalidOperationException( $"Cannot combine images with operation {combineOp}" ), + } ); return ( width, height ); } - private static Vector4 CappedVector( IReadOnlyList< byte > bytes, int offset, Matrix4x4 transform, bool invert ) + private static Vector4 CappedVector( IReadOnlyList< byte > bytes, int offset, Matrix4x4 transform, Vector4 constant ) { if( bytes.Count == 0 ) { @@ -110,11 +236,7 @@ public partial class CombinedTexture } var rgba = new Rgba32( bytes[ offset ], bytes[ offset + 1 ], bytes[ offset + 2 ], bytes[ offset + 3 ] ); - var transformed = Vector4.Transform( rgba.ToVector4(), transform ); - if( invert ) - { - transformed = new Vector4( 1 - transformed.X, 1 - transformed.Y, 1 - transformed.Z, transformed.W ); - } + var transformed = Vector4.Transform( rgba.ToVector4(), transform ) + constant; transformed.X = Math.Clamp( transformed.X, 0, 1 ); transformed.Y = Math.Clamp( transformed.Y, 0, 1 ); @@ -138,8 +260,8 @@ public partial class CombinedTexture public void DrawMatrixInputLeft( float width ) { - var ret = DrawMatrixInput( ref _multiplierLeft, width ); - ret |= ImGui.Checkbox( "Invert Colors##Left", ref _invertLeft ); + var ret = DrawMatrixInput( ref _multiplierLeft, ref _constantLeft, width ); + ret |= DrawMatrixTools( ref _multiplierLeft, ref _constantLeft ); if( ret ) { Update(); @@ -148,23 +270,56 @@ public partial class CombinedTexture public void DrawMatrixInputRight( float width ) { - var ret = DrawMatrixInput( ref _multiplierRight, width ); - ret |= ImGui.Checkbox( "Invert Colors##Right", ref _invertRight ); - ImGui.SameLine(); - ImGui.SetNextItemWidth( 75 ); + var ret = DrawMatrixInput( ref _multiplierRight, ref _constantRight, width ); + ret |= DrawMatrixTools( ref _multiplierRight, ref _constantRight ); + ImGui.SetNextItemWidth( 75.0f * UiHelpers.Scale ); ImGui.DragInt( "##XOffset", ref _offsetX, 0.5f ); ret |= ImGui.IsItemDeactivatedAfterEdit(); ImGui.SameLine(); - ImGui.SetNextItemWidth( 75 ); + ImGui.SetNextItemWidth( 75.0f * UiHelpers.Scale ); ImGui.DragInt( "Offsets##YOffset", ref _offsetY, 0.5f ); ret |= ImGui.IsItemDeactivatedAfterEdit(); + ImGui.SetNextItemWidth( 200.0f * UiHelpers.Scale ); + using( var c = ImRaii.Combo( "Combine Operation", CombineOpLabels[ (int)_combineOp ] ) ) + { + if( c ) + { + foreach( var op in Enum.GetValues() ) + { + if ( (int)op < 0 ) // Negative codes are for internal use only. + continue; + + if( ImGui.Selectable( CombineOpLabels[ (int)op ], op == _combineOp ) ) + { + _combineOp = op; + ret = true; + } + + ImGuiUtil.SelectableHelpMarker( CombineOpTooltips[ (int)op ] ); + } + } + } + using( var dis = ImRaii.Disabled( _combineOp != CombineOp.CopyChannels )) + { + ImGui.TextUnformatted( "Copy" ); + foreach( var channel in Enum.GetValues() ) + { + ImGui.SameLine(); + var copy = ( _copyChannels & channel ) != 0; + if( ImGui.Checkbox( channel.ToString(), ref copy ) ) + { + _copyChannels = copy ? ( _copyChannels | channel ) : ( _copyChannels & ~channel ); + ret = true; + } + } + } if( ret ) { Update(); } } - private static bool DrawMatrixInput( ref Matrix4x4 multiplier, float width ) + private static bool DrawMatrixInput( ref Matrix4x4 multiplier, ref Vector4 constant, float width ) { using var table = ImRaii.Table( string.Empty, 5, ImGuiTableFlags.BordersInner | ImGuiTableFlags.SizingFixedFit ); if( !table ) @@ -217,6 +372,110 @@ public partial class CombinedTexture changes |= DragFloat( "##AB", inputWidth, ref multiplier.M43 ); changes |= DragFloat( "##AA", inputWidth, ref multiplier.M44 ); + ImGui.TableNextColumn(); + ImGui.AlignTextToFramePadding(); + ImGui.Text( "1 " ); + changes |= DragFloat( "##1R", inputWidth, ref constant.X ); + changes |= DragFloat( "##1G", inputWidth, ref constant.Y ); + changes |= DragFloat( "##1B", inputWidth, ref constant.Z ); + changes |= DragFloat( "##1A", inputWidth, ref constant.W ); + return changes; } + + private static bool DrawMatrixTools( ref Matrix4x4 multiplier, ref Vector4 constant ) + { + var changes = false; + + using( var combo = ImRaii.Combo( "Presets", string.Empty, ImGuiComboFlags.NoPreview ) ) + { + if( combo ) + { + foreach( var (label, preMultiplier, preConstant) in PredefinedColorTransforms ) + { + if( ImGui.Selectable( label, multiplier == preMultiplier && constant == preConstant ) ) + { + multiplier = preMultiplier; + constant = preConstant; + changes = true; + } + } + } + } + + ImGui.SameLine(); + ImGui.Dummy( ImGuiHelpers.ScaledVector2( 20, 0 ) ); + ImGui.SameLine(); + ImGui.TextUnformatted( "Invert" ); + ImGui.SameLine(); + if( ImGui.Button( "Colors" ) ) + { + InvertRed( ref multiplier, ref constant ); + InvertGreen( ref multiplier, ref constant ); + InvertBlue( ref multiplier, ref constant ); + changes = true; + } + ImGui.SameLine(); + if( ImGui.Button( "R" ) ) + { + InvertRed( ref multiplier, ref constant ); + changes = true; + } + ImGui.SameLine(); + if( ImGui.Button( "G" ) ) + { + InvertGreen( ref multiplier, ref constant ); + changes = true; + } + ImGui.SameLine(); + if( ImGui.Button( "B" ) ) + { + InvertBlue( ref multiplier, ref constant ); + changes = true; + } + ImGui.SameLine(); + if( ImGui.Button( "A" ) ) + { + InvertAlpha( ref multiplier, ref constant ); + changes = true; + } + + return changes; + } + + private static void InvertRed( ref Matrix4x4 multiplier, ref Vector4 constant ) + { + multiplier.M11 = -multiplier.M11; + multiplier.M21 = -multiplier.M21; + multiplier.M31 = -multiplier.M31; + multiplier.M41 = -multiplier.M41; + constant.X = 1.0f - constant.X; + } + + private static void InvertGreen( ref Matrix4x4 multiplier, ref Vector4 constant ) + { + multiplier.M12 = -multiplier.M12; + multiplier.M22 = -multiplier.M22; + multiplier.M32 = -multiplier.M32; + multiplier.M42 = -multiplier.M42; + constant.Y = 1.0f - constant.Y; + } + + private static void InvertBlue( ref Matrix4x4 multiplier, ref Vector4 constant ) + { + multiplier.M13 = -multiplier.M13; + multiplier.M23 = -multiplier.M23; + multiplier.M33 = -multiplier.M33; + multiplier.M43 = -multiplier.M43; + constant.Z = 1.0f - constant.Z; + } + + private static void InvertAlpha( ref Matrix4x4 multiplier, ref Vector4 constant ) + { + multiplier.M14 = -multiplier.M14; + multiplier.M24 = -multiplier.M24; + multiplier.M34 = -multiplier.M34; + multiplier.M44 = -multiplier.M44; + constant.W = 1.0f - constant.W; + } } \ No newline at end of file diff --git a/Penumbra/Import/Textures/CombinedTexture.cs b/Penumbra/Import/Textures/CombinedTexture.cs index c26cb900..14a8a41c 100644 --- a/Penumbra/Import/Textures/CombinedTexture.cs +++ b/Penumbra/Import/Textures/CombinedTexture.cs @@ -1,4 +1,5 @@ using System; +using System.IO; using System.Numerics; using System.Threading.Tasks; @@ -68,6 +69,38 @@ public partial class CombinedTexture : IDisposable _current.TextureWrap!.Height); } + public void SaveAs(TextureType? texType, TextureManager textures, string path, TextureSaveType type, bool mipMaps) + { + TextureType finalTexType; + if (texType.HasValue) + finalTexType = texType.Value; + else + { + finalTexType = Path.GetExtension(path).ToLowerInvariant() switch + { + ".tex" => TextureType.Tex, + ".dds" => TextureType.Dds, + ".png" => TextureType.Png, + _ => TextureType.Unknown, + }; + } + + switch (finalTexType) + { + case TextureType.Tex: + SaveAsTex(textures, path, type, mipMaps); + break; + case TextureType.Dds: + SaveAsDds(textures, path, type, mipMaps); + break; + case TextureType.Png: + SaveAsPng(textures, path); + break; + default: + throw new ArgumentException($"Cannot save texture as TextureType {finalTexType} with extension {Path.GetExtension(path).ToLowerInvariant()}"); + } + } + public void SaveAsTex(TextureManager textures, string path, TextureSaveType type, bool mipMaps) => SaveAs(textures, path, type, mipMaps, true); @@ -97,36 +130,22 @@ public partial class CombinedTexture : IDisposable public void Update() { Clean(); - if (_left.IsLoaded) + switch (GetActualCombineOp()) { - if (_right.IsLoaded) - { - _current = _centerStorage; - _mode = Mode.Custom; - } - else if (!_invertLeft && _multiplierLeft.IsIdentity) - { + case CombineOp.Invalid: + break; + case CombineOp.LeftCopy: _mode = Mode.LeftCopy; _current = _left; - } - else - { - _current = _centerStorage; - _mode = Mode.Custom; - } - } - else if (_right.IsLoaded) - { - if (!_invertRight && _multiplierRight.IsIdentity) - { - _current = _right; + break; + case CombineOp.RightCopy: _mode = Mode.RightCopy; - } - else - { - _current = _centerStorage; + _current = _right; + break; + default: _mode = Mode.Custom; - } + _current = _centerStorage; + break; } } diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs index 4d36ff8a..4cf3731d 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs @@ -90,7 +90,7 @@ public partial class ModEditWindow if (ImGui.Selectable(newText, idx == _currentSaveAs)) _currentSaveAs = idx; - ImGuiUtil.HoverTooltip(newDesc); + ImGuiUtil.SelectableHelpMarker(newDesc); } } @@ -114,73 +114,65 @@ public partial class ModEditWindow SaveAsCombo(); ImGui.SameLine(); MipMapInput(); - if (ImGui.Button("Save as TEX", -Vector2.UnitX)) + + var canSaveInPlace = Path.IsPathRooted(_left.Path) && _left.Type is TextureType.Tex or TextureType.Dds or TextureType.Png; + + var buttonSize2 = new Vector2((ImGui.GetContentRegionAvail().X - ImGui.GetStyle().ItemSpacing.X) / 2, 0); + if (ImGuiUtil.DrawDisabledButton("Save in place", buttonSize2, + "This saves the texture in place. This is not revertible.", + !canSaveInPlace || _center.IsLeftCopy && _currentSaveAs == (int)CombinedTexture.TextureSaveType.AsIs)) { - var fileName = Path.GetFileNameWithoutExtension(_left.Path.Length > 0 ? _left.Path : _right.Path); - _fileDialog.OpenSavePicker("Save Texture as TEX...", ".tex", fileName, ".tex", (a, b) => - { - if (a) - _center.SaveAsTex(_textures, b, (CombinedTexture.TextureSaveType)_currentSaveAs, _addMipMaps); - }, _mod!.ModPath.FullName, _forceTextureStartPath); - _forceTextureStartPath = false; + _center.SaveAs(_left.Type, _textures, _left.Path, (CombinedTexture.TextureSaveType)_currentSaveAs, _addMipMaps); + AddReloadTask(_left.Path, false); } - if (ImGui.Button("Save as DDS", -Vector2.UnitX)) + ImGui.SameLine(); + if (ImGui.Button("Save as TEX, DDS or PNG", buttonSize2)) { - var fileName = Path.GetFileNameWithoutExtension(_right.Path.Length > 0 ? _right.Path : _left.Path); - _fileDialog.OpenSavePicker("Save Texture as DDS...", ".dds", fileName, ".dds", (a, b) => + var fileName = Path.GetFileNameWithoutExtension(_left.Path.Length > 0 ? _left.Path : _right.Path); + _fileDialog.OpenSavePicker("Save Texture as TEX, DDS or PNG...", "Textures{.png,.dds,.tex},.tex,.dds,.png", fileName, ".tex", (a, b) => { if (a) - _center.SaveAsDds(_textures, b, (CombinedTexture.TextureSaveType)_currentSaveAs, _addMipMaps); + { + _center.SaveAs(null, _textures, b, (CombinedTexture.TextureSaveType)_currentSaveAs, _addMipMaps); + if (b == _left.Path) + AddReloadTask(_left.Path, false); + else if (b == _right.Path) + AddReloadTask(_right.Path, true); + } }, _mod!.ModPath.FullName, _forceTextureStartPath); _forceTextureStartPath = false; } ImGui.NewLine(); - if (ImGui.Button("Save as PNG", -Vector2.UnitX)) + var canConvertInPlace = canSaveInPlace && _left.Type is TextureType.Tex && _center.IsLeftCopy; + + var buttonSize3 = new Vector2((ImGui.GetContentRegionAvail().X - ImGui.GetStyle().ItemSpacing.X * 2) / 3, 0); + if (ImGuiUtil.DrawDisabledButton("Convert to BC7", buttonSize3, + "This converts the texture to BC7 format in place. This is not revertible.", + !canConvertInPlace || _left.Format is DXGIFormat.BC7Typeless or DXGIFormat.BC7UNorm or DXGIFormat.BC7UNormSRGB)) { - var fileName = Path.GetFileNameWithoutExtension(_right.Path.Length > 0 ? _right.Path : _left.Path); - _fileDialog.OpenSavePicker("Save Texture as PNG...", ".png", fileName, ".png", (a, b) => - { - if (a) - _center.SaveAsPng(_textures, b); - }, _mod!.ModPath.FullName, _forceTextureStartPath); - _forceTextureStartPath = false; + _center.SaveAsTex(_textures, _left.Path, CombinedTexture.TextureSaveType.BC7, _left.MipMaps > 1); + AddReloadTask(_left.Path, false); } - if (_left.Type is TextureType.Tex && _center.IsLeftCopy) + ImGui.SameLine(); + if (ImGuiUtil.DrawDisabledButton("Convert to BC3", buttonSize3, + "This converts the texture to BC3 format in place. This is not revertible.", + !canConvertInPlace || _left.Format is DXGIFormat.BC3Typeless or DXGIFormat.BC3UNorm or DXGIFormat.BC3UNormSRGB)) { - 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(_textures, _left.Path, CombinedTexture.TextureSaveType.BC7, _left.MipMaps > 1); - AddReloadTask(_left.Path); - } - - ImGui.SameLine(); - if (ImGuiUtil.DrawDisabledButton("Convert to BC3", buttonSize, - "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(_textures, _left.Path, CombinedTexture.TextureSaveType.BC3, _left.MipMaps > 1); - AddReloadTask(_left.Path); - } - - ImGui.SameLine(); - if (ImGuiUtil.DrawDisabledButton("Convert to RGBA", buttonSize, - "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(_textures, _left.Path, CombinedTexture.TextureSaveType.Bitmap, _left.MipMaps > 1); - AddReloadTask(_left.Path); - } + _center.SaveAsTex(_textures, _left.Path, CombinedTexture.TextureSaveType.BC3, _left.MipMaps > 1); + AddReloadTask(_left.Path, false); } - else + + ImGui.SameLine(); + if (ImGuiUtil.DrawDisabledButton("Convert to RGBA", buttonSize3, + "This converts the texture to RGBA format in place. This is not revertible.", + !canConvertInPlace || _left.Format is DXGIFormat.B8G8R8A8UNorm or DXGIFormat.B8G8R8A8Typeless or DXGIFormat.B8G8R8A8UNormSRGB)) { - ImGui.NewLine(); + _center.SaveAsTex(_textures, _left.Path, CombinedTexture.TextureSaveType.Bitmap, _left.MipMaps > 1); + AddReloadTask(_left.Path, false); } } @@ -212,17 +204,19 @@ public partial class ModEditWindow _center.Draw(_textures, imageSize); } - private void AddReloadTask(string path) + private void AddReloadTask(string path, bool right) { _center.SaveTask.ContinueWith(t => { if (!t.IsCompletedSuccessfully) return; - if (_left.Path != path) + var tex = right ? _right : _left; + + if (tex.Path != path) return; - _dalamud.Framework.RunOnFrameworkThread(() => _left.Reload(_textures)); + _dalamud.Framework.RunOnFrameworkThread(() => tex.Reload(_textures)); }); } From 87c5164367443dc43a9b1dc7ede61ca5b2c93f84 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 25 Aug 2023 17:56:48 +0200 Subject: [PATCH 02/15] Small cleanup, auto-formatting. --- .../Textures/CombinedTexture.Manipulation.cs | 380 +++++++++--------- Penumbra/Import/Textures/CombinedTexture.cs | 15 +- .../AdvancedWindow/ModEditWindow.Textures.cs | 24 +- 3 files changed, 205 insertions(+), 214 deletions(-) diff --git a/Penumbra/Import/Textures/CombinedTexture.Manipulation.cs b/Penumbra/Import/Textures/CombinedTexture.Manipulation.cs index f8608071..2af2a8e4 100644 --- a/Penumbra/Import/Textures/CombinedTexture.Manipulation.cs +++ b/Penumbra/Import/Textures/CombinedTexture.Manipulation.cs @@ -66,25 +66,28 @@ public partial class CombinedTexture private const float GWeight = 0.7152f; private const float BWeight = 0.0722f; - private static readonly IReadOnlyList<(string Label, Matrix4x4 Multiplier, Vector4 Constant)> PredefinedColorTransforms = new (string, Matrix4x4, Vector4)[] - { - ("No Transform (Identity)", Matrix4x4.Identity, Vector4.Zero), - ("Grayscale (Average)", new Matrix4x4(OneThird, OneThird, OneThird, 0.0f, OneThird, OneThird, OneThird, 0.0f, OneThird, OneThird, OneThird, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f), Vector4.Zero), - ("Grayscale (Weighted)", new Matrix4x4(RWeight, RWeight, RWeight, 0.0f, GWeight, GWeight, GWeight, 0.0f, BWeight, BWeight, BWeight, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f), Vector4.Zero), - ("Grayscale (Average) to Alpha", new Matrix4x4(OneThird, OneThird, OneThird, OneThird, OneThird, OneThird, OneThird, OneThird, OneThird, OneThird, OneThird, OneThird, 0.0f, 0.0f, 0.0f, 0.0f), Vector4.Zero), - ("Grayscale (Weighted) to Alpha", new Matrix4x4(RWeight, RWeight, RWeight, RWeight, GWeight, GWeight, GWeight, GWeight, BWeight, BWeight, BWeight, BWeight, 0.0f, 0.0f, 0.0f, 0.0f), Vector4.Zero), - ("Extract Red", new Matrix4x4(1.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f), Vector4.UnitW), - ("Extract Green", new Matrix4x4(0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f), Vector4.UnitW), - ("Extract Blue", new Matrix4x4(0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f), Vector4.UnitW), - ("Extract Alpha", new Matrix4x4(0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f, 0.0f), Vector4.UnitW), - }; + // @formatter:off + private static readonly IReadOnlyList<(string Label, Matrix4x4 Multiplier, Vector4 Constant)> PredefinedColorTransforms = + new[] + { + ("No Transform (Identity)", Matrix4x4.Identity, Vector4.Zero ), + ("Grayscale (Average)", new Matrix4x4(OneThird, OneThird, OneThird, 0.0f, OneThird, OneThird, OneThird, 0.0f, OneThird, OneThird, OneThird, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f), Vector4.Zero ), + ("Grayscale (Weighted)", new Matrix4x4(RWeight, RWeight, RWeight, 0.0f, GWeight, GWeight, GWeight, 0.0f, BWeight, BWeight, BWeight, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f), Vector4.Zero ), + ("Grayscale (Average) to Alpha", new Matrix4x4(OneThird, OneThird, OneThird, OneThird, OneThird, OneThird, OneThird, OneThird, OneThird, OneThird, OneThird, OneThird, 0.0f, 0.0f, 0.0f, 0.0f), Vector4.Zero ), + ("Grayscale (Weighted) to Alpha", new Matrix4x4(RWeight, RWeight, RWeight, RWeight, GWeight, GWeight, GWeight, GWeight, BWeight, BWeight, BWeight, BWeight, 0.0f, 0.0f, 0.0f, 0.0f), Vector4.Zero ), + ("Extract Red", new Matrix4x4(1.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f), Vector4.UnitW ), + ("Extract Green", new Matrix4x4(0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f), Vector4.UnitW ), + ("Extract Blue", new Matrix4x4(0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f), Vector4.UnitW ), + ("Extract Alpha", new Matrix4x4(0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f, 0.0f), Vector4.UnitW ), + }; + // @formatter:on private CombineOp GetActualCombineOp() { var combineOp = (_left.IsLoaded, _right.IsLoaded) switch { - (true, true) => _combineOp, - (true, false) => CombineOp.LeftMultiply, + (true, true) => _combineOp, + (true, false) => CombineOp.LeftMultiply, (false, true) => CombineOp.RightMultiply, (false, false) => CombineOp.Invalid, }; @@ -99,111 +102,109 @@ public partial class CombinedTexture return combineOp switch { - CombineOp.LeftMultiply => (_multiplierLeft.IsIdentity && _constantLeft == Vector4.Zero) ? CombineOp.LeftCopy : CombineOp.LeftMultiply, - CombineOp.RightMultiply => (_multiplierRight.IsIdentity && _constantRight == Vector4.Zero) ? CombineOp.RightCopy : CombineOp.RightMultiply, - _ => combineOp, + CombineOp.LeftMultiply when _multiplierLeft.IsIdentity && _constantLeft == Vector4.Zero => CombineOp.LeftCopy, + CombineOp.RightMultiply when _multiplierRight.IsIdentity && _constantRight == Vector4.Zero => CombineOp.RightCopy, + _ => combineOp, }; } - private Vector4 DataLeft( int offset ) - => CappedVector( _left.RgbaPixels, offset, _multiplierLeft, _constantLeft ); + private Vector4 DataLeft(int offset) + => CappedVector(_left.RgbaPixels, offset, _multiplierLeft, _constantLeft); - private Vector4 DataRight( int offset ) - => CappedVector( _right.RgbaPixels, offset, _multiplierRight, _constantRight ); + private Vector4 DataRight(int offset) + => CappedVector(_right.RgbaPixels, offset, _multiplierRight, _constantRight); - private Vector4 DataRight( int x, int y ) + private Vector4 DataRight(int x, int y) { x += _offsetX; y += _offsetY; - if( x < 0 || x >= _right.TextureWrap!.Width || y < 0 || y >= _right.TextureWrap!.Height ) - { + if (x < 0 || x >= _right.TextureWrap!.Width || y < 0 || y >= _right.TextureWrap!.Height) return Vector4.Zero; - } - var offset = ( y * _right.TextureWrap!.Width + x ) * 4; - return CappedVector( _right.RgbaPixels, offset, _multiplierRight, _constantRight ); + var offset = (y * _right.TextureWrap!.Width + x) * 4; + return CappedVector(_right.RgbaPixels, offset, _multiplierRight, _constantRight); } - private void AddPixelsMultiplied( int y, ParallelLoopState _ ) + private void AddPixelsMultiplied(int y, ParallelLoopState _) { - for( var x = 0; x < _left.TextureWrap!.Width; ++x ) + for (var x = 0; x < _left.TextureWrap!.Width; ++x) { - var offset = ( _left.TextureWrap!.Width * y + x ) * 4; - var left = DataLeft( offset ); - var right = DataRight( x, y ); - var alpha = right.W + left.W * ( 1 - right.W ); + var offset = (_left.TextureWrap!.Width * y + x) * 4; + var left = DataLeft(offset); + var right = DataRight(x, y); + var alpha = right.W + left.W * (1 - right.W); 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; + : 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; } } - private void ReverseAddPixelsMultiplied( int y, ParallelLoopState _ ) + private void ReverseAddPixelsMultiplied(int y, ParallelLoopState _) { - for( var x = 0; x < _left.TextureWrap!.Width; ++x ) + for (var x = 0; x < _left.TextureWrap!.Width; ++x) { - var offset = ( _left.TextureWrap!.Width * y + x ) * 4; - var left = DataLeft( offset ); - var right = DataRight( x, y ); - var alpha = left.W + right.W * ( 1 - left.W ); + var offset = (_left.TextureWrap!.Width * y + x) * 4; + var left = DataLeft(offset); + var right = DataRight(x, y); + var alpha = left.W + right.W * (1 - left.W); var rgba = alpha == 0 ? new Rgba32() - : new Rgba32( ( ( left * left.W + right * right.W * ( 1 - left.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; + : new Rgba32(((left * left.W + right * right.W * (1 - left.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; } } - private void ChannelMergePixelsMultiplied( int y, ParallelLoopState _ ) + private void ChannelMergePixelsMultiplied(int y, ParallelLoopState _) { var channels = _copyChannels; - for( var x = 0; x < _left.TextureWrap!.Width; ++x ) + for (var x = 0; x < _left.TextureWrap!.Width; ++x) { - var offset = ( _left.TextureWrap!.Width * y + x ) * 4; - var left = DataLeft( offset ); - var right = DataRight( x, y ); - var rgba = new Rgba32( ( channels & Channels.Red ) != 0 ? right.X : left.X, - ( channels & Channels.Green ) != 0 ? right.Y : left.Y, - ( channels & Channels.Blue ) != 0 ? right.Z : left.Z, - ( channels & Channels.Alpha ) != 0 ? right.W : left.W ); - _centerStorage.RgbaPixels[ offset ] = rgba.R; - _centerStorage.RgbaPixels[ offset + 1 ] = rgba.G; - _centerStorage.RgbaPixels[ offset + 2 ] = rgba.B; - _centerStorage.RgbaPixels[ offset + 3 ] = rgba.A; + var offset = (_left.TextureWrap!.Width * y + x) * 4; + var left = DataLeft(offset); + var right = DataRight(x, y); + var rgba = new Rgba32((channels & Channels.Red) != 0 ? right.X : left.X, + (channels & Channels.Green) != 0 ? right.Y : left.Y, + (channels & Channels.Blue) != 0 ? right.Z : left.Z, + (channels & Channels.Alpha) != 0 ? right.W : left.W); + _centerStorage.RgbaPixels[offset] = rgba.R; + _centerStorage.RgbaPixels[offset + 1] = rgba.G; + _centerStorage.RgbaPixels[offset + 2] = rgba.B; + _centerStorage.RgbaPixels[offset + 3] = rgba.A; } } - private void MultiplyPixelsLeft( int y, ParallelLoopState _ ) + private void MultiplyPixelsLeft(int y, ParallelLoopState _) { - for( var x = 0; x < _left.TextureWrap!.Width; ++x ) + for (var x = 0; x < _left.TextureWrap!.Width; ++x) { - 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; + 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; } } - private void MultiplyPixelsRight( int y, ParallelLoopState _ ) + private void MultiplyPixelsRight(int y, ParallelLoopState _) { - for( var x = 0; x < _right.TextureWrap!.Width; ++x ) + for (var x = 0; x < _right.TextureWrap!.Width; ++x) { - var offset = ( _right.TextureWrap!.Width * y + x ) * 4; - var right = DataRight( offset ); - var rgba = new Rgba32( right ); - _centerStorage.RgbaPixels[ offset ] = rgba.R; - _centerStorage.RgbaPixels[ offset + 1 ] = rgba.G; - _centerStorage.RgbaPixels[ offset + 2 ] = rgba.B; - _centerStorage.RgbaPixels[ offset + 3 ] = rgba.A; + var offset = (_right.TextureWrap!.Width * y + x) * 4; + var right = DataRight(offset); + var rgba = new Rgba32(right); + _centerStorage.RgbaPixels[offset] = rgba.R; + _centerStorage.RgbaPixels[offset + 1] = rgba.G; + _centerStorage.RgbaPixels[offset + 2] = rgba.B; + _centerStorage.RgbaPixels[offset + 3] = rgba.A; } } @@ -212,238 +213,231 @@ public partial class CombinedTexture { var combineOp = GetActualCombineOp(); var (width, height) = combineOp is not CombineOp.Invalid or CombineOp.RightCopy or CombineOp.RightMultiply - ? ( _left.TextureWrap!.Width, _left.TextureWrap!.Height ) - : ( _right.TextureWrap!.Width, _right.TextureWrap!.Height ); + ? (_left.TextureWrap!.Width, _left.TextureWrap!.Height) + : (_right.TextureWrap!.Width, _right.TextureWrap!.Height); _centerStorage.RgbaPixels = new byte[width * height * 4]; _centerStorage.Type = TextureType.Bitmap; - Parallel.For( 0, height, combineOp switch + Parallel.For(0, height, combineOp switch { CombineOp.Over => AddPixelsMultiplied, CombineOp.Under => ReverseAddPixelsMultiplied, CombineOp.LeftMultiply => MultiplyPixelsLeft, CombineOp.RightMultiply => MultiplyPixelsRight, CombineOp.CopyChannels => ChannelMergePixelsMultiplied, - _ => throw new InvalidOperationException( $"Cannot combine images with operation {combineOp}" ), - } ); + _ => throw new InvalidOperationException($"Cannot combine images with operation {combineOp}"), + }); - return ( width, height ); + return (width, height); } - private static Vector4 CappedVector( IReadOnlyList< byte > bytes, int offset, Matrix4x4 transform, Vector4 constant ) + + private static Vector4 CappedVector(IReadOnlyList bytes, int offset, Matrix4x4 transform, Vector4 constant) { - if( bytes.Count == 0 ) - { + if (bytes.Count == 0) return Vector4.Zero; - } - var rgba = new Rgba32( bytes[ offset ], bytes[ offset + 1 ], bytes[ offset + 2 ], bytes[ offset + 3 ] ); - var transformed = Vector4.Transform( rgba.ToVector4(), transform ) + constant; + var rgba = new Rgba32(bytes[offset], bytes[offset + 1], bytes[offset + 2], bytes[offset + 3]); + var transformed = Vector4.Transform(rgba.ToVector4(), transform) + constant; - transformed.X = Math.Clamp( transformed.X, 0, 1 ); - transformed.Y = Math.Clamp( transformed.Y, 0, 1 ); - transformed.Z = Math.Clamp( transformed.Z, 0, 1 ); - transformed.W = Math.Clamp( transformed.W, 0, 1 ); + transformed.X = Math.Clamp(transformed.X, 0, 1); + transformed.Y = Math.Clamp(transformed.Y, 0, 1); + transformed.Z = Math.Clamp(transformed.Z, 0, 1); + transformed.W = Math.Clamp(transformed.W, 0, 1); return transformed; } - private static bool DragFloat( string label, float width, ref float value ) + private static bool DragFloat(string label, float width, ref float value) { var tmp = value; ImGui.TableNextColumn(); - ImGui.SetNextItemWidth( width ); - if( ImGui.DragFloat( label, ref tmp, 0.001f, -1f, 1f ) ) - { + ImGui.SetNextItemWidth(width); + if (ImGui.DragFloat(label, ref tmp, 0.001f, -1f, 1f)) value = tmp; - } return ImGui.IsItemDeactivatedAfterEdit(); } - public void DrawMatrixInputLeft( float width ) + public void DrawMatrixInputLeft(float width) { - var ret = DrawMatrixInput( ref _multiplierLeft, ref _constantLeft, width ); - ret |= DrawMatrixTools( ref _multiplierLeft, ref _constantLeft ); - if( ret ) - { + var ret = DrawMatrixInput(ref _multiplierLeft, ref _constantLeft, width); + ret |= DrawMatrixTools(ref _multiplierLeft, ref _constantLeft); + if (ret) Update(); - } } - public void DrawMatrixInputRight( float width ) + public void DrawMatrixInputRight(float width) { - var ret = DrawMatrixInput( ref _multiplierRight, ref _constantRight, width ); - ret |= DrawMatrixTools( ref _multiplierRight, ref _constantRight ); - ImGui.SetNextItemWidth( 75.0f * UiHelpers.Scale ); - ImGui.DragInt( "##XOffset", ref _offsetX, 0.5f ); + var ret = DrawMatrixInput(ref _multiplierRight, ref _constantRight, width); + ret |= DrawMatrixTools(ref _multiplierRight, ref _constantRight); + ImGui.SetNextItemWidth(75.0f * UiHelpers.Scale); + ImGui.DragInt("##XOffset", ref _offsetX, 0.5f); ret |= ImGui.IsItemDeactivatedAfterEdit(); ImGui.SameLine(); - ImGui.SetNextItemWidth( 75.0f * UiHelpers.Scale ); - ImGui.DragInt( "Offsets##YOffset", ref _offsetY, 0.5f ); + ImGui.SetNextItemWidth(75.0f * UiHelpers.Scale); + ImGui.DragInt("Offsets##YOffset", ref _offsetY, 0.5f); ret |= ImGui.IsItemDeactivatedAfterEdit(); - ImGui.SetNextItemWidth( 200.0f * UiHelpers.Scale ); - using( var c = ImRaii.Combo( "Combine Operation", CombineOpLabels[ (int)_combineOp ] ) ) + ImGui.SetNextItemWidth(200.0f * UiHelpers.Scale); + using (var c = ImRaii.Combo("Combine Operation", CombineOpLabels[(int)_combineOp])) { - if( c ) - { - foreach( var op in Enum.GetValues() ) + if (c) + foreach (var op in Enum.GetValues()) { - if ( (int)op < 0 ) // Negative codes are for internal use only. + if ((int)op < 0) // Negative codes are for internal use only. continue; - if( ImGui.Selectable( CombineOpLabels[ (int)op ], op == _combineOp ) ) + if (ImGui.Selectable(CombineOpLabels[(int)op], op == _combineOp)) { _combineOp = op; ret = true; } - ImGuiUtil.SelectableHelpMarker( CombineOpTooltips[ (int)op ] ); + ImGuiUtil.SelectableHelpMarker(CombineOpTooltips[(int)op]); } - } } - using( var dis = ImRaii.Disabled( _combineOp != CombineOp.CopyChannels )) + + using (var dis = ImRaii.Disabled(_combineOp != CombineOp.CopyChannels)) { - ImGui.TextUnformatted( "Copy" ); - foreach( var channel in Enum.GetValues() ) + ImGui.TextUnformatted("Copy"); + foreach (var channel in Enum.GetValues()) { ImGui.SameLine(); - var copy = ( _copyChannels & channel ) != 0; - if( ImGui.Checkbox( channel.ToString(), ref copy ) ) + var copy = (_copyChannels & channel) != 0; + if (ImGui.Checkbox(channel.ToString(), ref copy)) { - _copyChannels = copy ? ( _copyChannels | channel ) : ( _copyChannels & ~channel ); - ret = true; + _copyChannels = copy ? _copyChannels | channel : _copyChannels & ~channel; + ret = true; } } } - if( ret ) - { + + if (ret) Update(); - } } - private static bool DrawMatrixInput( ref Matrix4x4 multiplier, ref Vector4 constant, float width ) + private static bool DrawMatrixInput(ref Matrix4x4 multiplier, ref Vector4 constant, float width) { - using var table = ImRaii.Table( string.Empty, 5, ImGuiTableFlags.BordersInner | ImGuiTableFlags.SizingFixedFit ); - if( !table ) - { + using var table = ImRaii.Table(string.Empty, 5, ImGuiTableFlags.BordersInner | ImGuiTableFlags.SizingFixedFit); + if (!table) return false; - } var changes = false; ImGui.TableNextColumn(); ImGui.TableNextColumn(); - ImGuiUtil.Center( "R" ); + ImGuiUtil.Center("R"); ImGui.TableNextColumn(); - ImGuiUtil.Center( "G" ); + ImGuiUtil.Center("G"); ImGui.TableNextColumn(); - ImGuiUtil.Center( "B" ); + ImGuiUtil.Center("B"); ImGui.TableNextColumn(); - ImGuiUtil.Center( "A" ); + ImGuiUtil.Center("A"); var inputWidth = width / 6; ImGui.TableNextColumn(); ImGui.AlignTextToFramePadding(); - ImGui.Text( "R " ); - changes |= DragFloat( "##RR", inputWidth, ref multiplier.M11 ); - changes |= DragFloat( "##RG", inputWidth, ref multiplier.M12 ); - changes |= DragFloat( "##RB", inputWidth, ref multiplier.M13 ); - changes |= DragFloat( "##RA", inputWidth, ref multiplier.M14 ); + ImGui.Text("R "); + changes |= DragFloat("##RR", inputWidth, ref multiplier.M11); + changes |= DragFloat("##RG", inputWidth, ref multiplier.M12); + changes |= DragFloat("##RB", inputWidth, ref multiplier.M13); + changes |= DragFloat("##RA", inputWidth, ref multiplier.M14); ImGui.TableNextColumn(); ImGui.AlignTextToFramePadding(); - ImGui.Text( "G " ); - changes |= DragFloat( "##GR", inputWidth, ref multiplier.M21 ); - changes |= DragFloat( "##GG", inputWidth, ref multiplier.M22 ); - changes |= DragFloat( "##GB", inputWidth, ref multiplier.M23 ); - changes |= DragFloat( "##GA", inputWidth, ref multiplier.M24 ); + ImGui.Text("G "); + changes |= DragFloat("##GR", inputWidth, ref multiplier.M21); + changes |= DragFloat("##GG", inputWidth, ref multiplier.M22); + changes |= DragFloat("##GB", inputWidth, ref multiplier.M23); + changes |= DragFloat("##GA", inputWidth, ref multiplier.M24); ImGui.TableNextColumn(); ImGui.AlignTextToFramePadding(); - ImGui.Text( "B " ); - changes |= DragFloat( "##BR", inputWidth, ref multiplier.M31 ); - changes |= DragFloat( "##BG", inputWidth, ref multiplier.M32 ); - changes |= DragFloat( "##BB", inputWidth, ref multiplier.M33 ); - changes |= DragFloat( "##BA", inputWidth, ref multiplier.M34 ); + ImGui.Text("B "); + changes |= DragFloat("##BR", inputWidth, ref multiplier.M31); + changes |= DragFloat("##BG", inputWidth, ref multiplier.M32); + changes |= DragFloat("##BB", inputWidth, ref multiplier.M33); + changes |= DragFloat("##BA", inputWidth, ref multiplier.M34); ImGui.TableNextColumn(); ImGui.AlignTextToFramePadding(); - ImGui.Text( "A " ); - changes |= DragFloat( "##AR", inputWidth, ref multiplier.M41 ); - changes |= DragFloat( "##AG", inputWidth, ref multiplier.M42 ); - changes |= DragFloat( "##AB", inputWidth, ref multiplier.M43 ); - changes |= DragFloat( "##AA", inputWidth, ref multiplier.M44 ); + ImGui.Text("A "); + changes |= DragFloat("##AR", inputWidth, ref multiplier.M41); + changes |= DragFloat("##AG", inputWidth, ref multiplier.M42); + changes |= DragFloat("##AB", inputWidth, ref multiplier.M43); + changes |= DragFloat("##AA", inputWidth, ref multiplier.M44); ImGui.TableNextColumn(); ImGui.AlignTextToFramePadding(); - ImGui.Text( "1 " ); - changes |= DragFloat( "##1R", inputWidth, ref constant.X ); - changes |= DragFloat( "##1G", inputWidth, ref constant.Y ); - changes |= DragFloat( "##1B", inputWidth, ref constant.Z ); - changes |= DragFloat( "##1A", inputWidth, ref constant.W ); + ImGui.Text("1 "); + changes |= DragFloat("##1R", inputWidth, ref constant.X); + changes |= DragFloat("##1G", inputWidth, ref constant.Y); + changes |= DragFloat("##1B", inputWidth, ref constant.Z); + changes |= DragFloat("##1A", inputWidth, ref constant.W); return changes; } - private static bool DrawMatrixTools( ref Matrix4x4 multiplier, ref Vector4 constant ) + private static bool DrawMatrixTools(ref Matrix4x4 multiplier, ref Vector4 constant) { var changes = false; - using( var combo = ImRaii.Combo( "Presets", string.Empty, ImGuiComboFlags.NoPreview ) ) + using (var combo = ImRaii.Combo("Presets", string.Empty, ImGuiComboFlags.NoPreview)) { - if( combo ) - { - foreach( var (label, preMultiplier, preConstant) in PredefinedColorTransforms ) + if (combo) + foreach (var (label, preMultiplier, preConstant) in PredefinedColorTransforms) { - if( ImGui.Selectable( label, multiplier == preMultiplier && constant == preConstant ) ) + if (ImGui.Selectable(label, multiplier == preMultiplier && constant == preConstant)) { multiplier = preMultiplier; constant = preConstant; changes = true; } } - } } ImGui.SameLine(); - ImGui.Dummy( ImGuiHelpers.ScaledVector2( 20, 0 ) ); + ImGui.Dummy(ImGuiHelpers.ScaledVector2(20, 0)); ImGui.SameLine(); - ImGui.TextUnformatted( "Invert" ); + ImGui.TextUnformatted("Invert"); ImGui.SameLine(); - if( ImGui.Button( "Colors" ) ) + if (ImGui.Button("Colors")) { - InvertRed( ref multiplier, ref constant ); - InvertGreen( ref multiplier, ref constant ); - InvertBlue( ref multiplier, ref constant ); + InvertRed(ref multiplier, ref constant); + InvertGreen(ref multiplier, ref constant); + InvertBlue(ref multiplier, ref constant); changes = true; } + ImGui.SameLine(); - if( ImGui.Button( "R" ) ) + if (ImGui.Button("R")) { - InvertRed( ref multiplier, ref constant ); + InvertRed(ref multiplier, ref constant); changes = true; } + ImGui.SameLine(); - if( ImGui.Button( "G" ) ) + if (ImGui.Button("G")) { - InvertGreen( ref multiplier, ref constant ); + InvertGreen(ref multiplier, ref constant); changes = true; } + ImGui.SameLine(); - if( ImGui.Button( "B" ) ) + if (ImGui.Button("B")) { - InvertBlue( ref multiplier, ref constant ); + InvertBlue(ref multiplier, ref constant); changes = true; } + ImGui.SameLine(); - if( ImGui.Button( "A" ) ) + if (ImGui.Button("A")) { - InvertAlpha( ref multiplier, ref constant ); + InvertAlpha(ref multiplier, ref constant); changes = true; } return changes; } - private static void InvertRed( ref Matrix4x4 multiplier, ref Vector4 constant ) + private static void InvertRed(ref Matrix4x4 multiplier, ref Vector4 constant) { multiplier.M11 = -multiplier.M11; multiplier.M21 = -multiplier.M21; @@ -452,7 +446,7 @@ public partial class CombinedTexture constant.X = 1.0f - constant.X; } - private static void InvertGreen( ref Matrix4x4 multiplier, ref Vector4 constant ) + private static void InvertGreen(ref Matrix4x4 multiplier, ref Vector4 constant) { multiplier.M12 = -multiplier.M12; multiplier.M22 = -multiplier.M22; @@ -461,7 +455,7 @@ public partial class CombinedTexture constant.Y = 1.0f - constant.Y; } - private static void InvertBlue( ref Matrix4x4 multiplier, ref Vector4 constant ) + private static void InvertBlue(ref Matrix4x4 multiplier, ref Vector4 constant) { multiplier.M13 = -multiplier.M13; multiplier.M23 = -multiplier.M23; @@ -470,7 +464,7 @@ public partial class CombinedTexture constant.Z = 1.0f - constant.Z; } - private static void InvertAlpha( ref Matrix4x4 multiplier, ref Vector4 constant ) + private static void InvertAlpha(ref Matrix4x4 multiplier, ref Vector4 constant) { multiplier.M14 = -multiplier.M14; multiplier.M24 = -multiplier.M24; @@ -478,4 +472,4 @@ public partial class CombinedTexture multiplier.M44 = -multiplier.M44; constant.W = 1.0f - constant.W; } -} \ No newline at end of file +} diff --git a/Penumbra/Import/Textures/CombinedTexture.cs b/Penumbra/Import/Textures/CombinedTexture.cs index 14a8a41c..b7e2a90a 100644 --- a/Penumbra/Import/Textures/CombinedTexture.cs +++ b/Penumbra/Import/Textures/CombinedTexture.cs @@ -71,19 +71,14 @@ public partial class CombinedTexture : IDisposable public void SaveAs(TextureType? texType, TextureManager textures, string path, TextureSaveType type, bool mipMaps) { - TextureType finalTexType; - if (texType.HasValue) - finalTexType = texType.Value; - else - { - finalTexType = Path.GetExtension(path).ToLowerInvariant() switch + var finalTexType = texType + ?? Path.GetExtension(path).ToLowerInvariant() switch { ".tex" => TextureType.Tex, ".dds" => TextureType.Dds, ".png" => TextureType.Png, _ => TextureType.Unknown, }; - } switch (finalTexType) { @@ -97,7 +92,8 @@ public partial class CombinedTexture : IDisposable SaveAsPng(textures, path); break; default: - throw new ArgumentException($"Cannot save texture as TextureType {finalTexType} with extension {Path.GetExtension(path).ToLowerInvariant()}"); + throw new ArgumentException( + $"Cannot save texture as TextureType {finalTexType} with extension {Path.GetExtension(path).ToLowerInvariant()}"); } } @@ -132,8 +128,7 @@ public partial class CombinedTexture : IDisposable Clean(); switch (GetActualCombineOp()) { - case CombineOp.Invalid: - break; + case CombineOp.Invalid: break; case CombineOp.LeftCopy: _mode = Mode.LeftCopy; _current = _left; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs index 4cf3731d..af256f5c 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs @@ -130,17 +130,18 @@ public partial class ModEditWindow if (ImGui.Button("Save as TEX, DDS or PNG", buttonSize2)) { var fileName = Path.GetFileNameWithoutExtension(_left.Path.Length > 0 ? _left.Path : _right.Path); - _fileDialog.OpenSavePicker("Save Texture as TEX, DDS or PNG...", "Textures{.png,.dds,.tex},.tex,.dds,.png", fileName, ".tex", (a, b) => - { - if (a) + _fileDialog.OpenSavePicker("Save Texture as TEX, DDS or PNG...", "Textures{.png,.dds,.tex},.tex,.dds,.png", fileName, ".tex", + (a, b) => { - _center.SaveAs(null, _textures, b, (CombinedTexture.TextureSaveType)_currentSaveAs, _addMipMaps); - if (b == _left.Path) - AddReloadTask(_left.Path, false); - else if (b == _right.Path) - AddReloadTask(_right.Path, true); - } - }, _mod!.ModPath.FullName, _forceTextureStartPath); + if (a) + { + _center.SaveAs(null, _textures, b, (CombinedTexture.TextureSaveType)_currentSaveAs, _addMipMaps); + if (b == _left.Path) + AddReloadTask(_left.Path, false); + else if (b == _right.Path) + AddReloadTask(_right.Path, true); + } + }, _mod!.ModPath.FullName, _forceTextureStartPath); _forceTextureStartPath = false; } @@ -169,7 +170,8 @@ public partial class ModEditWindow ImGui.SameLine(); if (ImGuiUtil.DrawDisabledButton("Convert to RGBA", buttonSize3, "This converts the texture to RGBA format in place. This is not revertible.", - !canConvertInPlace || _left.Format is DXGIFormat.B8G8R8A8UNorm or DXGIFormat.B8G8R8A8Typeless or DXGIFormat.B8G8R8A8UNormSRGB)) + !canConvertInPlace + || _left.Format is DXGIFormat.B8G8R8A8UNorm or DXGIFormat.B8G8R8A8Typeless or DXGIFormat.B8G8R8A8UNormSRGB)) { _center.SaveAsTex(_textures, _left.Path, CombinedTexture.TextureSaveType.Bitmap, _left.MipMaps > 1); AddReloadTask(_left.Path, false); From 781bbb3d26ebaa0400b92d7996a8c0563a1d8ccd Mon Sep 17 00:00:00 2001 From: Exter-N Date: Fri, 25 Aug 2023 17:59:18 +0200 Subject: [PATCH 03/15] Textures: Un-merge save buttons, make ignore unselectable --- .../Textures/CombinedTexture.Manipulation.cs | 8 ++-- .../AdvancedWindow/ModEditWindow.Textures.cs | 41 +++++++++++-------- 2 files changed, 28 insertions(+), 21 deletions(-) diff --git a/Penumbra/Import/Textures/CombinedTexture.Manipulation.cs b/Penumbra/Import/Textures/CombinedTexture.Manipulation.cs index f8608071..3256836d 100644 --- a/Penumbra/Import/Textures/CombinedTexture.Manipulation.cs +++ b/Penumbra/Import/Textures/CombinedTexture.Manipulation.cs @@ -15,12 +15,12 @@ public partial class CombinedTexture { private enum CombineOp { + LeftMultiply = -4, LeftCopy = -3, RightCopy = -2, Invalid = -1, Over = 0, Under = 1, - LeftMultiply = 2, RightMultiply = 3, CopyChannels = 4, } @@ -47,7 +47,6 @@ public partial class CombinedTexture { "Overlay over Input", "Input over Overlay", - "Ignore Overlay", "Replace Input", "Copy Channels", }; @@ -55,9 +54,8 @@ public partial class CombinedTexture private static readonly IReadOnlyList CombineOpTooltips = new string[] { "Standard composition.\nApply the overlay over the input.", - "Standard composition, reversed.\nApply the input over the overlay.", - "Use only the input, and ignore the overlay.", - "Completely replace the input with the overlay.", + "Standard composition, reversed.\nApply the input over the overlay ; can be used to fix some wrong imports.", + "Completely replace the input with the overlay.\nCan be used to select the destination file as input and the source file as overlay.", "Replace some input channels with those from the overlay.\nUseful for Multi maps.", }; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs index 4cf3731d..3db2d407 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs @@ -127,22 +127,14 @@ public partial class ModEditWindow } ImGui.SameLine(); - if (ImGui.Button("Save as TEX, DDS or PNG", buttonSize2)) - { - var fileName = Path.GetFileNameWithoutExtension(_left.Path.Length > 0 ? _left.Path : _right.Path); - _fileDialog.OpenSavePicker("Save Texture as TEX, DDS or PNG...", "Textures{.png,.dds,.tex},.tex,.dds,.png", fileName, ".tex", (a, b) => - { - if (a) - { - _center.SaveAs(null, _textures, b, (CombinedTexture.TextureSaveType)_currentSaveAs, _addMipMaps); - if (b == _left.Path) - AddReloadTask(_left.Path, false); - else if (b == _right.Path) - AddReloadTask(_right.Path, true); - } - }, _mod!.ModPath.FullName, _forceTextureStartPath); - _forceTextureStartPath = false; - } + if (ImGui.Button("Save as TEX", buttonSize2)) + OpenSaveAsDialog(".tex"); + + if (ImGui.Button("Export as PNG", buttonSize2)) + OpenSaveAsDialog(".png"); + ImGui.SameLine(); + if (ImGui.Button("Export as DDS", buttonSize2)) + OpenSaveAsDialog(".dds"); ImGui.NewLine(); @@ -204,6 +196,23 @@ public partial class ModEditWindow _center.Draw(_textures, imageSize); } + private void OpenSaveAsDialog(string defaultExtension) + { + var fileName = Path.GetFileNameWithoutExtension(_left.Path.Length > 0 ? _left.Path : _right.Path); + _fileDialog.OpenSavePicker("Save Texture as TEX, DDS or PNG...", "Textures{.png,.dds,.tex},.tex,.dds,.png", fileName, defaultExtension, (a, b) => + { + if (a) + { + _center.SaveAs(null, _textures, b, (CombinedTexture.TextureSaveType)_currentSaveAs, _addMipMaps); + if (b == _left.Path) + AddReloadTask(_left.Path, false); + else if (b == _right.Path) + AddReloadTask(_right.Path, true); + } + }, _mod!.ModPath.FullName, _forceTextureStartPath); + _forceTextureStartPath = false; + } + private void AddReloadTask(string path, bool right) { _center.SaveTask.ContinueWith(t => From 792707a6e3237e55d909ca05e0e58a7c4d736743 Mon Sep 17 00:00:00 2001 From: Exter-N Date: Fri, 25 Aug 2023 18:24:21 +0200 Subject: [PATCH 04/15] Textures: Renumber CombineOps. Positive values in this enum also double as indices into the labels and tooltip arrays. (confirmed skill issue moment) --- Penumbra/Import/Textures/CombinedTexture.Manipulation.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Penumbra/Import/Textures/CombinedTexture.Manipulation.cs b/Penumbra/Import/Textures/CombinedTexture.Manipulation.cs index fed269da..057d0234 100644 --- a/Penumbra/Import/Textures/CombinedTexture.Manipulation.cs +++ b/Penumbra/Import/Textures/CombinedTexture.Manipulation.cs @@ -21,8 +21,8 @@ public partial class CombinedTexture Invalid = -1, Over = 0, Under = 1, - RightMultiply = 3, - CopyChannels = 4, + RightMultiply = 2, + CopyChannels = 3, } [Flags] From 99b43bf577d58744525264b9dcd3957cfb76dd5b Mon Sep 17 00:00:00 2001 From: Exter-N Date: Fri, 25 Aug 2023 20:09:16 +0200 Subject: [PATCH 05/15] Textures: Automatic resizing --- .../Textures/CombinedTexture.Manipulation.cs | 131 ++++++++++++++---- 1 file changed, 105 insertions(+), 26 deletions(-) diff --git a/Penumbra/Import/Textures/CombinedTexture.Manipulation.cs b/Penumbra/Import/Textures/CombinedTexture.Manipulation.cs index 057d0234..b02ba7b7 100644 --- a/Penumbra/Import/Textures/CombinedTexture.Manipulation.cs +++ b/Penumbra/Import/Textures/CombinedTexture.Manipulation.cs @@ -8,6 +8,8 @@ using OtterGui; using SixLabors.ImageSharp.PixelFormats; using Dalamud.Interface; using Penumbra.UI; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Processing; namespace Penumbra.Import.Textures; @@ -25,6 +27,13 @@ public partial class CombinedTexture CopyChannels = 3, } + private enum ResizeOp + { + None = 0, + ToLeft = 1, + ToRight = 2, + } + [Flags] private enum Channels { @@ -41,8 +50,16 @@ public partial class CombinedTexture private int _offsetX = 0; private int _offsetY = 0; private CombineOp _combineOp = CombineOp.Over; + private ResizeOp _resizeOp = ResizeOp.None; private Channels _copyChannels = Channels.Red | Channels.Green | Channels.Blue | Channels.Alpha; + private int _rightWidth = 0; + private int _rightHeight = 0; + private int _targetWidth = 0; + private int _targetHeight = 0; + private byte[] _leftPixels = Array.Empty(); + private byte[] _rightPixels = Array.Empty(); + private static readonly IReadOnlyList CombineOpLabels = new string[] { "Overlay over Input", @@ -59,6 +76,26 @@ public partial class CombinedTexture "Replace some input channels with those from the overlay.\nUseful for Multi maps.", }; + private static (bool UsesLeft, bool UsesRight) GetCombineOpFlags(CombineOp combineOp) + => combineOp switch + { + CombineOp.LeftCopy => (true, false), + CombineOp.LeftMultiply => (true, false), + CombineOp.RightCopy => (false, true), + CombineOp.RightMultiply => (false, true), + CombineOp.Over => (true, true), + CombineOp.Under => (true, true), + CombineOp.CopyChannels => (true, true), + _ => throw new ArgumentException($"Invalid combine operation {combineOp}"), + }; + + private static readonly IReadOnlyList ResizeOpLabels = new string[] + { + "No Resizing", + "Adjust Overlay to Input", + "Adjust Input to Overlay", + }; + private const float OneThird = 1.0f / 3.0f; private const float RWeight = 0.2126f; private const float GWeight = 0.7152f; @@ -107,27 +144,27 @@ public partial class CombinedTexture } private Vector4 DataLeft(int offset) - => CappedVector(_left.RgbaPixels, offset, _multiplierLeft, _constantLeft); + => CappedVector(_leftPixels, offset, _multiplierLeft, _constantLeft); private Vector4 DataRight(int offset) - => CappedVector(_right.RgbaPixels, offset, _multiplierRight, _constantRight); + => CappedVector(_rightPixels, offset, _multiplierRight, _constantRight); private Vector4 DataRight(int x, int y) { x += _offsetX; y += _offsetY; - if (x < 0 || x >= _right.TextureWrap!.Width || y < 0 || y >= _right.TextureWrap!.Height) + if (x < 0 || x >= _rightWidth || y < 0 || y >= _rightHeight) return Vector4.Zero; - var offset = (y * _right.TextureWrap!.Width + x) * 4; - return CappedVector(_right.RgbaPixels, offset, _multiplierRight, _constantRight); + var offset = (y * _rightWidth + x) * 4; + return CappedVector(_rightPixels, offset, _multiplierRight, _constantRight); } private void AddPixelsMultiplied(int y, ParallelLoopState _) { - for (var x = 0; x < _left.TextureWrap!.Width; ++x) + for (var x = 0; x < _targetWidth; ++x) { - var offset = (_left.TextureWrap!.Width * y + x) * 4; + var offset = (_targetWidth * y + x) * 4; var left = DataLeft(offset); var right = DataRight(x, y); var alpha = right.W + left.W * (1 - right.W); @@ -143,9 +180,9 @@ public partial class CombinedTexture private void ReverseAddPixelsMultiplied(int y, ParallelLoopState _) { - for (var x = 0; x < _left.TextureWrap!.Width; ++x) + for (var x = 0; x < _targetWidth; ++x) { - var offset = (_left.TextureWrap!.Width * y + x) * 4; + var offset = (_targetWidth * y + x) * 4; var left = DataLeft(offset); var right = DataRight(x, y); var alpha = left.W + right.W * (1 - left.W); @@ -162,9 +199,9 @@ public partial class CombinedTexture private void ChannelMergePixelsMultiplied(int y, ParallelLoopState _) { var channels = _copyChannels; - for (var x = 0; x < _left.TextureWrap!.Width; ++x) + for (var x = 0; x < _targetWidth; ++x) { - var offset = (_left.TextureWrap!.Width * y + x) * 4; + var offset = (_targetWidth * y + x) * 4; var left = DataLeft(offset); var right = DataRight(x, y); var rgba = new Rgba32((channels & Channels.Red) != 0 ? right.X : left.X, @@ -180,9 +217,9 @@ public partial class CombinedTexture private void MultiplyPixelsLeft(int y, ParallelLoopState _) { - for (var x = 0; x < _left.TextureWrap!.Width; ++x) + for (var x = 0; x < _targetWidth; ++x) { - var offset = (_left.TextureWrap!.Width * y + x) * 4; + var offset = (_targetWidth * y + x) * 4; var left = DataLeft(offset); var rgba = new Rgba32(left); _centerStorage.RgbaPixels[offset] = rgba.R; @@ -194,9 +231,9 @@ public partial class CombinedTexture private void MultiplyPixelsRight(int y, ParallelLoopState _) { - for (var x = 0; x < _right.TextureWrap!.Width; ++x) + for (var x = 0; x < _targetWidth; ++x) { - var offset = (_right.TextureWrap!.Width * y + x) * 4; + var offset = (_targetWidth * y + x) * 4; var right = DataRight(offset); var rgba = new Rgba32(right); _centerStorage.RgbaPixels[offset] = rgba.R; @@ -206,26 +243,59 @@ public partial class CombinedTexture } } + private byte[] ResizePixels(byte[] rgbaPixels, int sourceWidth, int sourceHeight) + { + if (sourceWidth == _targetWidth && sourceHeight == _targetHeight) + return rgbaPixels; + + byte[] resizedPixels; + using (var image = Image.LoadPixelData(rgbaPixels, sourceWidth, sourceHeight)) + { + image.Mutate(ctx => ctx.Resize(_targetWidth, _targetHeight)); + + resizedPixels = new byte[_targetWidth * _targetHeight * 4]; + image.CopyPixelDataTo(resizedPixels); + } + + return resizedPixels; + } + private (int Width, int Height) CombineImage() { var combineOp = GetActualCombineOp(); - var (width, height) = combineOp is not CombineOp.Invalid or CombineOp.RightCopy or CombineOp.RightMultiply + var (usesLeft, usesRight) = GetCombineOpFlags(combineOp); + var resizeOp = usesLeft && usesRight ? _resizeOp : ResizeOp.None; + (_targetWidth, _targetHeight) = usesLeft && resizeOp != ResizeOp.ToRight ? (_left.TextureWrap!.Width, _left.TextureWrap!.Height) : (_right.TextureWrap!.Width, _right.TextureWrap!.Height); - _centerStorage.RgbaPixels = new byte[width * height * 4]; + _centerStorage.RgbaPixels = new byte[_targetWidth * _targetHeight * 4]; _centerStorage.Type = TextureType.Bitmap; - Parallel.For(0, height, combineOp switch + try { - CombineOp.Over => AddPixelsMultiplied, - CombineOp.Under => ReverseAddPixelsMultiplied, - CombineOp.LeftMultiply => MultiplyPixelsLeft, - CombineOp.RightMultiply => MultiplyPixelsRight, - CombineOp.CopyChannels => ChannelMergePixelsMultiplied, - _ => throw new InvalidOperationException($"Cannot combine images with operation {combineOp}"), - }); + if (usesLeft) + _leftPixels = (resizeOp == ResizeOp.ToRight) ? ResizePixels(_left.RgbaPixels, _left.TextureWrap!.Width, _left.TextureWrap!.Height) : _left.RgbaPixels; + if (usesRight) + (_rightWidth, _rightHeight, _rightPixels) = (resizeOp == ResizeOp.ToLeft) + ? (_targetWidth, _targetHeight, ResizePixels(_right.RgbaPixels, _right.TextureWrap!.Width, _right.TextureWrap!.Height)) + : (_right.TextureWrap!.Width, _right.TextureWrap!.Height, _right.RgbaPixels); + Parallel.For(0, _targetHeight, combineOp switch + { + CombineOp.Over => AddPixelsMultiplied, + CombineOp.Under => ReverseAddPixelsMultiplied, + CombineOp.LeftMultiply => MultiplyPixelsLeft, + CombineOp.RightMultiply => MultiplyPixelsRight, + CombineOp.CopyChannels => ChannelMergePixelsMultiplied, + _ => throw new InvalidOperationException($"Cannot combine images with operation {combineOp}"), + }); + } + finally + { + _leftPixels = Array.Empty(); + _rightPixels = Array.Empty(); + } - return (width, height); + return (_targetWidth, _targetHeight); } private static Vector4 CappedVector(IReadOnlyList bytes, int offset, Matrix4x4 transform, Vector4 constant) @@ -266,6 +336,7 @@ public partial class CombinedTexture { var ret = DrawMatrixInput(ref _multiplierRight, ref _constantRight, width); ret |= DrawMatrixTools(ref _multiplierRight, ref _constantRight); + ImGui.SetNextItemWidth(75.0f * UiHelpers.Scale); ImGui.DragInt("##XOffset", ref _offsetX, 0.5f); ret |= ImGui.IsItemDeactivatedAfterEdit(); @@ -273,6 +344,7 @@ public partial class CombinedTexture ImGui.SetNextItemWidth(75.0f * UiHelpers.Scale); ImGui.DragInt("Offsets##YOffset", ref _offsetY, 0.5f); ret |= ImGui.IsItemDeactivatedAfterEdit(); + ImGui.SetNextItemWidth(200.0f * UiHelpers.Scale); using (var c = ImRaii.Combo("Combine Operation", CombineOpLabels[(int)_combineOp])) { @@ -292,6 +364,13 @@ public partial class CombinedTexture } } + var (usesLeft, usesRight) = GetCombineOpFlags(_combineOp); + using (var dis = ImRaii.Disabled(!usesLeft || !usesRight)) + { + ret |= ImGuiUtil.GenericEnumCombo("Resizing Mode", 200.0f * UiHelpers.Scale, _resizeOp, out _resizeOp, + Enum.GetValues(), op => ResizeOpLabels[(int)op]); + } + using (var dis = ImRaii.Disabled(_combineOp != CombineOp.CopyChannels)) { ImGui.TextUnformatted("Copy"); From ead88f9fa6313ea83082b9c51d9ad4d886f1fd61 Mon Sep 17 00:00:00 2001 From: Exter-N Date: Sun, 27 Aug 2023 03:42:34 +0200 Subject: [PATCH 06/15] Skin Fixer (fixes modding of skin.shpk) --- Penumbra/Interop/Services/CharacterUtility.cs | 12 +- Penumbra/Interop/Services/SkinFixer.cs | 161 ++++++++++++++++++ .../Interop/Structs/CharacterUtilityData.cs | 8 +- Penumbra/Interop/Structs/MtrlResource.cs | 22 ++- Penumbra/Interop/Structs/ResourceHandle.cs | 14 +- Penumbra/Penumbra.cs | 5 +- Penumbra/Services/ServiceManager.cs | 3 +- Penumbra/UI/Tabs/DebugTab.cs | 107 +++++++++--- 8 files changed, 302 insertions(+), 30 deletions(-) create mode 100644 Penumbra/Interop/Services/SkinFixer.cs diff --git a/Penumbra/Interop/Services/CharacterUtility.cs b/Penumbra/Interop/Services/CharacterUtility.cs index ef706f6d..00eab531 100644 --- a/Penumbra/Interop/Services/CharacterUtility.cs +++ b/Penumbra/Interop/Services/CharacterUtility.cs @@ -33,6 +33,7 @@ public unsafe partial class CharacterUtility : IDisposable public event Action LoadingFinished; public nint DefaultTransparentResource { get; private set; } public nint DefaultDecalResource { get; private set; } + public nint DefaultSkinShpkResource { get; private set; } /// /// The relevant indices depend on which meta manipulations we allow for. @@ -102,6 +103,12 @@ public unsafe partial class CharacterUtility : IDisposable anyMissing |= DefaultDecalResource == nint.Zero; } + if (DefaultSkinShpkResource == nint.Zero) + { + DefaultSkinShpkResource = (nint)Address->SkinShpkResource; + anyMissing |= DefaultSkinShpkResource == nint.Zero; + } + if (anyMissing) return; @@ -140,15 +147,16 @@ public unsafe partial class CharacterUtility : IDisposable /// Return all relevant resources to the default resource. public void ResetAll() - { + { if (!Ready) - return; + return; foreach (var list in _lists) list.Dispose(); Address->TransparentTexResource = (TextureResourceHandle*)DefaultTransparentResource; Address->DecalTexResource = (TextureResourceHandle*)DefaultDecalResource; + Address->SkinShpkResource = (ResourceHandle*)DefaultSkinShpkResource; } public void Dispose() diff --git a/Penumbra/Interop/Services/SkinFixer.cs b/Penumbra/Interop/Services/SkinFixer.cs new file mode 100644 index 00000000..5ce35f29 --- /dev/null +++ b/Penumbra/Interop/Services/SkinFixer.cs @@ -0,0 +1,161 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using System.Threading; +using Dalamud.Hooking; +using Dalamud.Utility.Signatures; +using FFXIVClientStructs.FFXIV.Client.Graphics.Render; +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; +using FFXIVClientStructs.FFXIV.Client.System.Resource; +using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; +using Penumbra.GameData; +using Penumbra.GameData.Enums; +using Penumbra.Interop.PathResolving; +using Penumbra.Interop.ResourceLoading; +using Penumbra.String.Classes; + +namespace Penumbra.Interop.Services; + +public unsafe class SkinFixer : IDisposable +{ + public static readonly Utf8GamePath SkinShpkPath = + Utf8GamePath.FromSpan("shader/sm5/shpk/skin.shpk"u8, out var p) ? p : Utf8GamePath.Empty; + + [Signature(Sigs.HumanVTable, ScanType = ScanType.StaticAddress)] + private readonly nint* _humanVTable = null!; + + private delegate nint OnRenderMaterialDelegate(nint drawObject, OnRenderMaterialParams* param); + + [StructLayout(LayoutKind.Explicit)] + private struct OnRenderMaterialParams + { + [FieldOffset(0x0)] + public Model* Model; + [FieldOffset(0x8)] + public uint MaterialIndex; + } + + private readonly Hook _onRenderMaterialHook; + + private readonly CollectionResolver _collectionResolver; + private readonly GameEventManager _gameEvents; + private readonly ResourceLoader _resources; + private readonly CharacterUtility _utility; + + private readonly ConcurrentDictionary _skinShpks = new(); + + private readonly object _lock = new(); + + private bool _enabled = true; + private int _moddedSkinShpkCount = 0; + private ulong _slowPathCallDelta = 0; + public bool Enabled + { + get => _enabled; + set => _enabled = value; + } + + public int ModdedSkinShpkCount + => _moddedSkinShpkCount; + + public SkinFixer(CollectionResolver collectionResolver, GameEventManager gameEvents, ResourceLoader resources, CharacterUtility utility, DrawObjectState _) + { + SignatureHelper.Initialise(this); + _collectionResolver = collectionResolver; + _gameEvents = gameEvents; + _resources = resources; + _utility = utility; + _onRenderMaterialHook = Hook.FromAddress(_humanVTable[62], OnRenderHumanMaterial); + _gameEvents.CharacterBaseCreated += OnCharacterBaseCreated; // The dependency on DrawObjectState shall ensure that this handler is registered after its one. + _gameEvents.CharacterBaseDestructor += OnCharacterBaseDestructor; + _onRenderMaterialHook.Enable(); + } + + ~SkinFixer() + { + Dispose(false); + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + protected virtual void Dispose(bool disposing) + { + _onRenderMaterialHook.Dispose(); + _gameEvents.CharacterBaseCreated -= OnCharacterBaseCreated; + _gameEvents.CharacterBaseDestructor -= OnCharacterBaseDestructor; + foreach (var skinShpk in _skinShpks.Values) + if (skinShpk != nint.Zero) + ((ResourceHandle*)skinShpk)->DecRef(); + _skinShpks.Clear(); + _moddedSkinShpkCount = 0; + } + + public ulong GetAndResetSlowPathCallDelta() + => Interlocked.Exchange(ref _slowPathCallDelta, 0); + + private void OnCharacterBaseCreated(uint modelCharaId, nint customize, nint equipment, nint drawObject) + { + if (((CharacterBase*)drawObject)->GetModelType() != CharacterBase.ModelType.Human) + return; + + nint skinShpk; + try + { + var data = _collectionResolver.IdentifyCollection((DrawObject*)drawObject, true); + skinShpk = data.Valid ? (nint)_resources.LoadResolvedResource(ResourceCategory.Shader, ResourceType.Shpk, SkinShpkPath.Path, data) : nint.Zero; + } + catch (Exception e) + { + Penumbra.Log.Error($"Error while resolving skin.shpk for human {drawObject:X}: {e}"); + skinShpk = nint.Zero; + } + + if (skinShpk != nint.Zero && _skinShpks.TryAdd(drawObject, skinShpk) && skinShpk != _utility.DefaultSkinShpkResource) + Interlocked.Increment(ref _moddedSkinShpkCount); + } + + private void OnCharacterBaseDestructor(nint characterBase) + { + if (_skinShpks.Remove(characterBase, out var skinShpk) && skinShpk != nint.Zero) + { + ((ResourceHandle*)skinShpk)->DecRef(); + if (skinShpk != _utility.DefaultSkinShpkResource) + Interlocked.Decrement(ref _moddedSkinShpkCount); + } + } + + private nint OnRenderHumanMaterial(nint human, OnRenderMaterialParams* param) + { + if (!_enabled || // Can be toggled on the debug tab. + _moddedSkinShpkCount == 0 || // If we don't have any on-screen instances of modded skin.shpk, we don't need the slow path at all. + !_skinShpks.TryGetValue(human, out var skinShpk) || skinShpk == nint.Zero) + return _onRenderMaterialHook!.Original(human, param); + + var material = param->Model->Materials[param->MaterialIndex]; + var shpkResource = ((Structs.MtrlResource*)material->MaterialResourceHandle)->ShpkResourceHandle; + if ((nint)shpkResource != skinShpk) + return _onRenderMaterialHook!.Original(human, param); + + Interlocked.Increment(ref _slowPathCallDelta); + + // Performance considerations: + // - This function is called from several threads simultaneously, hence the need for synchronization in the swapping path ; + // - Function is called each frame for each material on screen, after culling, i. e. up to thousands of times a frame in crowded areas ; + // - Swapping path is taken up to hundreds of times a frame. + // At the time of writing, the lock doesn't seem to have a noticeable impact in either framerate or CPU usage, but the swapping path shall still be avoided as much as possible. + lock (_lock) + try + { + _utility.Address->SkinShpkResource = (Structs.ResourceHandle*)skinShpk; + return _onRenderMaterialHook!.Original(human, param); + } + finally + { + _utility.Address->SkinShpkResource = (Structs.ResourceHandle*)_utility.DefaultSkinShpkResource; + } + } +} diff --git a/Penumbra/Interop/Structs/CharacterUtilityData.cs b/Penumbra/Interop/Structs/CharacterUtilityData.cs index b273091b..765ad25f 100644 --- a/Penumbra/Interop/Structs/CharacterUtilityData.cs +++ b/Penumbra/Interop/Structs/CharacterUtilityData.cs @@ -10,6 +10,7 @@ public unsafe struct CharacterUtilityData { public const int IndexTransparentTex = 72; public const int IndexDecalTex = 73; + public const int IndexSkinShpk = 76; public static readonly MetaIndex[] EqdpIndices = Enum.GetNames< MetaIndex >() .Zip( Enum.GetValues< MetaIndex >() ) @@ -17,8 +18,8 @@ public unsafe struct CharacterUtilityData .Select( n => n.Second ).ToArray(); public const int TotalNumResources = 87; - - /// Obtain the index for the eqdp file corresponding to the given race code and accessory. + + /// Obtain the index for the eqdp file corresponding to the given race code and accessory. public static MetaIndex EqdpIdx( GenderRace raceCode, bool accessory ) => +( int )raceCode switch { @@ -95,5 +96,8 @@ public unsafe struct CharacterUtilityData [FieldOffset( 8 + IndexDecalTex * 8 )] public TextureResourceHandle* DecalTexResource; + [FieldOffset( 8 + IndexSkinShpk * 8 )] + public ResourceHandle* SkinShpkResource; + // not included resources have no known use case. } \ No newline at end of file diff --git a/Penumbra/Interop/Structs/MtrlResource.cs b/Penumbra/Interop/Structs/MtrlResource.cs index 28756877..424adfe4 100644 --- a/Penumbra/Interop/Structs/MtrlResource.cs +++ b/Penumbra/Interop/Structs/MtrlResource.cs @@ -8,8 +8,11 @@ public unsafe struct MtrlResource [FieldOffset( 0x00 )] public ResourceHandle Handle; + [FieldOffset( 0xC8 )] + public ShaderPackageResourceHandle* ShpkResourceHandle; + [FieldOffset( 0xD0 )] - public ushort* TexSpace; // Contains the offsets for the tex files inside the string list. + public TextureEntry* TexSpace; // Contains the offsets for the tex files inside the string list. [FieldOffset( 0xE0 )] public byte* StringList; @@ -24,8 +27,21 @@ public unsafe struct MtrlResource => StringList + ShpkOffset; public byte* TexString( int idx ) - => StringList + *( TexSpace + 4 + idx * 8 ); + => StringList + TexSpace[idx].PathOffset; public bool TexIsDX11( int idx ) - => *(TexSpace + 5 + idx * 8) >= 0x8000; + => TexSpace[idx].Flags >= 0x8000; + + [StructLayout(LayoutKind.Explicit, Size = 0x10)] + public struct TextureEntry + { + [FieldOffset( 0x00 )] + public TextureResourceHandle* ResourceHandle; + + [FieldOffset( 0x08 )] + public ushort PathOffset; + + [FieldOffset( 0x0A )] + public ushort Flags; + } } \ No newline at end of file diff --git a/Penumbra/Interop/Structs/ResourceHandle.cs b/Penumbra/Interop/Structs/ResourceHandle.cs index 4de81903..5db0f8e1 100644 --- a/Penumbra/Interop/Structs/ResourceHandle.cs +++ b/Penumbra/Interop/Structs/ResourceHandle.cs @@ -1,5 +1,7 @@ using System; using System.Runtime.InteropServices; +using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel; +using FFXIVClientStructs.FFXIV.Client.Graphics.Render; using FFXIVClientStructs.FFXIV.Client.System.Resource; using Penumbra.GameData; using Penumbra.GameData.Enums; @@ -18,12 +20,22 @@ public unsafe struct TextureResourceHandle public IntPtr Unk; [FieldOffset( 0x118 )] - public IntPtr KernelTexture; + public Texture* KernelTexture; [FieldOffset( 0x20 )] public IntPtr NewKernelTexture; } +[StructLayout(LayoutKind.Explicit)] +public unsafe struct ShaderPackageResourceHandle +{ + [FieldOffset( 0x0 )] + public ResourceHandle Handle; + + [FieldOffset( 0xB0 )] + public ShaderPackage* ShaderPackage; +} + [StructLayout( LayoutKind.Explicit )] public unsafe struct ResourceHandle { diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index b291e392..eeee8fd0 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -22,8 +22,8 @@ using Penumbra.Collections.Manager; using Penumbra.UI.Tabs; using ChangedItemClick = Penumbra.Communication.ChangedItemClick; using ChangedItemHover = Penumbra.Communication.ChangedItemHover; -using OtterGui.Tasks; - +using OtterGui.Tasks; + namespace Penumbra; public class Penumbra : IDalamudPlugin @@ -81,6 +81,7 @@ public class Penumbra : IDalamudPlugin { _services.GetRequiredService(); } + _services.GetRequiredService(); SetupInterface(); SetupApi(); diff --git a/Penumbra/Services/ServiceManager.cs b/Penumbra/Services/ServiceManager.cs index 728585ae..8bea52e3 100644 --- a/Penumbra/Services/ServiceManager.cs +++ b/Penumbra/Services/ServiceManager.cs @@ -117,7 +117,8 @@ public static class ServiceManager => services.AddSingleton() .AddSingleton() .AddSingleton() - .AddSingleton(); + .AddSingleton() + .AddSingleton(); private static IServiceCollection AddResolvers(this IServiceCollection services) => services.AddSingleton() diff --git a/Penumbra/UI/Tabs/DebugTab.cs b/Penumbra/UI/Tabs/DebugTab.cs index a48fd714..1ee62c35 100644 --- a/Penumbra/UI/Tabs/DebugTab.cs +++ b/Penumbra/UI/Tabs/DebugTab.cs @@ -34,6 +34,9 @@ using CharacterBase = FFXIVClientStructs.FFXIV.Client.Graphics.Scene.CharacterBa using CharacterUtility = Penumbra.Interop.Services.CharacterUtility; using ObjectKind = Dalamud.Game.ClientState.Objects.Enums.ObjectKind; using ResidentResourceManager = Penumbra.Interop.Services.ResidentResourceManager; +using Penumbra.Interop.Services; +using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel; +using static Lumina.Data.Parsing.Layer.LayerCommon; namespace Penumbra.UI.Tabs; @@ -63,6 +66,7 @@ public class DebugTab : Window, ITab private readonly ImportPopup _importPopup; private readonly FrameworkManager _framework; private readonly TextureManager _textureManager; + private readonly SkinFixer _skinFixer; public DebugTab(StartTracker timer, PerformanceTracker performance, Configuration config, CollectionManager collectionManager, ValidityChecker validityChecker, ModManager modManager, HttpApi httpApi, ActorService actorService, @@ -70,7 +74,7 @@ public class DebugTab : Window, ITab ResourceManagerService resourceManager, PenumbraIpcProviders ipc, CollectionResolver collectionResolver, DrawObjectState drawObjectState, PathState pathState, SubfileHelper subfileHelper, IdentifiedCollectionCache identifiedCollectionCache, CutsceneService cutsceneService, ModImportManager modImporter, ImportPopup importPopup, FrameworkManager framework, - TextureManager textureManager) + TextureManager textureManager, SkinFixer skinFixer) : base("Penumbra Debug Window", ImGuiWindowFlags.NoCollapse, false) { IsOpen = true; @@ -103,6 +107,7 @@ public class DebugTab : Window, ITab _importPopup = importPopup; _framework = framework; _textureManager = textureManager; + _skinFixer = skinFixer; } public ReadOnlySpan Label @@ -144,6 +149,8 @@ public class DebugTab : Window, ITab ImGui.NewLine(); DrawPlayerModelInfo(); ImGui.NewLine(); + DrawGlobalVariableInfo(); + ImGui.NewLine(); DrawDebugTabIpc(); ImGui.NewLine(); } @@ -338,7 +345,7 @@ public class DebugTab : Window, ITab if (!ImGui.CollapsingHeader("Actors")) return; - using var table = Table("##actors", 4, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit, + using var table = Table("##actors", 5, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit, -Vector2.UnitX); if (!table) return; @@ -350,6 +357,7 @@ public class DebugTab : Window, ITab ImGuiUtil.DrawTableColumn(name); ImGuiUtil.DrawTableColumn(string.Empty); + ImGuiUtil.DrawTableColumn(string.Empty); ImGuiUtil.DrawTableColumn(_actorService.AwaitedService.ToString(id)); ImGuiUtil.DrawTableColumn(string.Empty); } @@ -363,6 +371,7 @@ public class DebugTab : Window, ITab { ImGuiUtil.DrawTableColumn($"{((GameObject*)obj.Address)->ObjectIndex}"); ImGuiUtil.DrawTableColumn($"0x{obj.Address:X}"); + ImGuiUtil.DrawTableColumn((obj.Address == nint.Zero) ? string.Empty : $"0x{(nint)((Character*)obj.Address)->GameObject.GetDrawObject():X}"); var identifier = _actorService.AwaitedService.FromObject(obj, false, true, false); ImGuiUtil.DrawTableColumn(_actorService.AwaitedService.ToString(identifier)); var id = obj.ObjectKind == ObjectKind.BattleNpc ? $"{identifier.DataId} | {obj.DataId}" : identifier.DataId.ToString(); @@ -582,45 +591,76 @@ public class DebugTab : Window, ITab if (!ImGui.CollapsingHeader("Character Utility")) return; - using var table = Table("##CharacterUtility", 6, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit, + var enableSkinFixer = _skinFixer.Enabled; + if (ImGui.Checkbox("Enable Skin Fixer", ref enableSkinFixer)) + _skinFixer.Enabled = enableSkinFixer; + + if (enableSkinFixer) + { + ImGui.SameLine(); + ImGui.Dummy(ImGuiHelpers.ScaledVector2(20, 0)); + ImGui.SameLine(); + ImGui.TextUnformatted($"\u0394 Slow-Path Calls: {_skinFixer.GetAndResetSlowPathCallDelta()}"); + ImGui.SameLine(); + ImGui.Dummy(ImGuiHelpers.ScaledVector2(20, 0)); + ImGui.SameLine(); + ImGui.TextUnformatted($"Draw Objects with Modded skin.shpk: {_skinFixer.ModdedSkinShpkCount}"); + } + + using var table = Table("##CharacterUtility", 7, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit, -Vector2.UnitX); if (!table) return; - for (var i = 0; i < CharacterUtility.RelevantIndices.Length; ++i) + for (var idx = 0; idx < CharacterUtility.ReverseIndices.Length; ++idx) { - var idx = CharacterUtility.RelevantIndices[i]; - var intern = new CharacterUtility.InternalIndex(i); + var intern = CharacterUtility.ReverseIndices[idx]; var resource = _characterUtility.Address->Resource(idx); ImGui.TableNextColumn(); + ImGui.TextUnformatted($"[{idx}]"); + ImGui.TableNextColumn(); ImGui.TextUnformatted($"0x{(ulong)resource:X}"); ImGui.TableNextColumn(); + if (resource == null) + { + ImGui.TableNextColumn(); + ImGui.TableNextColumn(); + ImGui.TableNextColumn(); + ImGui.TableNextColumn(); + continue; + } UiHelpers.Text(resource); ImGui.TableNextColumn(); - ImGui.Selectable($"0x{resource->GetData().Data:X}"); + var data = (nint)ResourceHandle.GetData(resource); + var length = ResourceHandle.GetLength(resource); + ImGui.Selectable($"0x{data:X}"); if (ImGui.IsItemClicked()) { - var (data, length) = resource->GetData(); if (data != nint.Zero && length > 0) ImGui.SetClipboardText(string.Join("\n", - new ReadOnlySpan((byte*)data, length).ToArray().Select(b => b.ToString("X2")))); + new ReadOnlySpan((byte*)data, (int)length).ToArray().Select(b => b.ToString("X2")))); } ImGuiUtil.HoverTooltip("Click to copy bytes to clipboard."); + ImGui.TableNextColumn(); + ImGui.TextUnformatted($"{length}"); ImGui.TableNextColumn(); - ImGui.TextUnformatted($"{resource->GetData().Length}"); - ImGui.TableNextColumn(); - ImGui.Selectable($"0x{_characterUtility.DefaultResource(intern).Address:X}"); - if (ImGui.IsItemClicked()) - ImGui.SetClipboardText(string.Join("\n", - new ReadOnlySpan((byte*)_characterUtility.DefaultResource(intern).Address, - _characterUtility.DefaultResource(intern).Size).ToArray().Select(b => b.ToString("X2")))); + if (intern.Value != -1) + { + ImGui.Selectable($"0x{_characterUtility.DefaultResource(intern).Address:X}"); + if (ImGui.IsItemClicked()) + ImGui.SetClipboardText(string.Join("\n", + new ReadOnlySpan((byte*)_characterUtility.DefaultResource(intern).Address, + _characterUtility.DefaultResource(intern).Size).ToArray().Select(b => b.ToString("X2")))); - ImGuiUtil.HoverTooltip("Click to copy bytes to clipboard."); + ImGuiUtil.HoverTooltip("Click to copy bytes to clipboard."); - ImGui.TableNextColumn(); - ImGui.TextUnformatted($"{_characterUtility.DefaultResource(intern).Size}"); + ImGui.TableNextColumn(); + ImGui.TextUnformatted($"{_characterUtility.DefaultResource(intern).Size}"); + } + else + ImGui.TableNextColumn(); } } @@ -665,6 +705,18 @@ public class DebugTab : Window, ITab } } + private static void DrawCopyableAddress(string label, nint address) + { + using (var _ = PushFont(UiBuilder.MonoFont)) + if (ImGui.Selectable($"0x{address:X16} {label}")) + ImGui.SetClipboardText($"{address:X16}"); + + ImGuiUtil.HoverTooltip("Click to copy address to clipboard."); + } + + private static unsafe void DrawCopyableAddress(string label, void* address) + => DrawCopyableAddress(label, (nint)address); + /// Draw information about the models, materials and resources currently loaded by the local player. private unsafe void DrawPlayerModelInfo() { @@ -673,10 +725,14 @@ public class DebugTab : Window, ITab if (!ImGui.CollapsingHeader($"Player Model Info: {name}##Draw") || player == null) return; + DrawCopyableAddress("PlayerCharacter", player.Address); + var model = (CharacterBase*)((Character*)player.Address)->GameObject.GetDrawObject(); if (model == null) return; + DrawCopyableAddress("CharacterBase", model); + using (var t1 = Table("##table", 2, ImGuiTableFlags.SizingFixedFit)) { if (t1) @@ -730,6 +786,19 @@ public class DebugTab : Window, ITab } } + /// Draw information about some game global variables. + private unsafe void DrawGlobalVariableInfo() + { + var header = ImGui.CollapsingHeader("Global Variables"); + ImGuiUtil.HoverTooltip("Draw information about global variables. Can provide useful starting points for a memory viewer."); + if (!header) + return; + + DrawCopyableAddress("CharacterUtility", _characterUtility.Address); + DrawCopyableAddress("ResidentResourceManager", _residentResources.Address); + DrawCopyableAddress("Device", Device.Instance()); + } + /// Draw resources with unusual reference count. private unsafe void DrawResourceProblems() { From ec14efb789d28d2c8bdddf10e94ad474222d7c0d Mon Sep 17 00:00:00 2001 From: Exter-N Date: Sun, 27 Aug 2023 04:04:14 +0200 Subject: [PATCH 07/15] Skin Fixer: Make resolving skin.shpk for new draw objects async --- Penumbra/Interop/Services/SkinFixer.cs | 28 +++++++++++++++----------- 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/Penumbra/Interop/Services/SkinFixer.cs b/Penumbra/Interop/Services/SkinFixer.cs index 5ce35f29..479feafc 100644 --- a/Penumbra/Interop/Services/SkinFixer.cs +++ b/Penumbra/Interop/Services/SkinFixer.cs @@ -3,6 +3,7 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Runtime.InteropServices; using System.Threading; +using System.Threading.Tasks; using Dalamud.Hooking; using Dalamud.Utility.Signatures; using FFXIVClientStructs.FFXIV.Client.Graphics.Render; @@ -102,20 +103,23 @@ public unsafe class SkinFixer : IDisposable if (((CharacterBase*)drawObject)->GetModelType() != CharacterBase.ModelType.Human) return; - nint skinShpk; - try + Task.Run(delegate { - var data = _collectionResolver.IdentifyCollection((DrawObject*)drawObject, true); - skinShpk = data.Valid ? (nint)_resources.LoadResolvedResource(ResourceCategory.Shader, ResourceType.Shpk, SkinShpkPath.Path, data) : nint.Zero; - } - catch (Exception e) - { - Penumbra.Log.Error($"Error while resolving skin.shpk for human {drawObject:X}: {e}"); - skinShpk = nint.Zero; - } + nint skinShpk; + try + { + var data = _collectionResolver.IdentifyCollection((DrawObject*)drawObject, true); + skinShpk = data.Valid ? (nint)_resources.LoadResolvedResource(ResourceCategory.Shader, ResourceType.Shpk, SkinShpkPath.Path, data) : nint.Zero; + } + catch (Exception e) + { + Penumbra.Log.Error($"Error while resolving skin.shpk for human {drawObject:X}: {e}"); + skinShpk = nint.Zero; + } - if (skinShpk != nint.Zero && _skinShpks.TryAdd(drawObject, skinShpk) && skinShpk != _utility.DefaultSkinShpkResource) - Interlocked.Increment(ref _moddedSkinShpkCount); + if (skinShpk != nint.Zero && _skinShpks.TryAdd(drawObject, skinShpk) && skinShpk != _utility.DefaultSkinShpkResource) + Interlocked.Increment(ref _moddedSkinShpkCount); + }); } private void OnCharacterBaseDestructor(nint characterBase) From 6c0864c8b9ae942566cc0d2f2368270c3eaeb98d Mon Sep 17 00:00:00 2001 From: Exter-N Date: Mon, 28 Aug 2023 01:54:14 +0200 Subject: [PATCH 08/15] Textures: Add a matrix preset that drops alpha --- Penumbra/Import/Textures/CombinedTexture.Manipulation.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Penumbra/Import/Textures/CombinedTexture.Manipulation.cs b/Penumbra/Import/Textures/CombinedTexture.Manipulation.cs index b02ba7b7..c36c5065 100644 --- a/Penumbra/Import/Textures/CombinedTexture.Manipulation.cs +++ b/Penumbra/Import/Textures/CombinedTexture.Manipulation.cs @@ -110,6 +110,7 @@ public partial class CombinedTexture ("Grayscale (Weighted)", new Matrix4x4(RWeight, RWeight, RWeight, 0.0f, GWeight, GWeight, GWeight, 0.0f, BWeight, BWeight, BWeight, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f), Vector4.Zero ), ("Grayscale (Average) to Alpha", new Matrix4x4(OneThird, OneThird, OneThird, OneThird, OneThird, OneThird, OneThird, OneThird, OneThird, OneThird, OneThird, OneThird, 0.0f, 0.0f, 0.0f, 0.0f), Vector4.Zero ), ("Grayscale (Weighted) to Alpha", new Matrix4x4(RWeight, RWeight, RWeight, RWeight, GWeight, GWeight, GWeight, GWeight, BWeight, BWeight, BWeight, BWeight, 0.0f, 0.0f, 0.0f, 0.0f), Vector4.Zero ), + ("Make Opaque (Drop Alpha)", new Matrix4x4(1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f), Vector4.UnitW ), ("Extract Red", new Matrix4x4(1.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f), Vector4.UnitW ), ("Extract Green", new Matrix4x4(0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f), Vector4.UnitW ), ("Extract Blue", new Matrix4x4(0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f), Vector4.UnitW ), From f54146ada43d118c64392b83c77145659769574f Mon Sep 17 00:00:00 2001 From: Exter-N Date: Mon, 28 Aug 2023 03:30:21 +0200 Subject: [PATCH 09/15] Textures: PR #327 feedback --- .../Textures/CombinedTexture.Manipulation.cs | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/Penumbra/Import/Textures/CombinedTexture.Manipulation.cs b/Penumbra/Import/Textures/CombinedTexture.Manipulation.cs index c36c5065..57749471 100644 --- a/Penumbra/Import/Textures/CombinedTexture.Manipulation.cs +++ b/Penumbra/Import/Textures/CombinedTexture.Manipulation.cs @@ -249,12 +249,10 @@ public partial class CombinedTexture if (sourceWidth == _targetWidth && sourceHeight == _targetHeight) return rgbaPixels; - byte[] resizedPixels; + var resizedPixels = new byte[_targetWidth * _targetHeight * 4]; using (var image = Image.LoadPixelData(rgbaPixels, sourceWidth, sourceHeight)) { - image.Mutate(ctx => ctx.Resize(_targetWidth, _targetHeight)); - - resizedPixels = new byte[_targetWidth * _targetHeight * 4]; + image.Mutate(ctx => ctx.Resize(_targetWidth, _targetHeight, KnownResamplers.Lanczos3)); image.CopyPixelDataTo(resizedPixels); } @@ -267,9 +265,13 @@ public partial class CombinedTexture var combineOp = GetActualCombineOp(); var (usesLeft, usesRight) = GetCombineOpFlags(combineOp); var resizeOp = usesLeft && usesRight ? _resizeOp : ResizeOp.None; - (_targetWidth, _targetHeight) = usesLeft && resizeOp != ResizeOp.ToRight - ? (_left.TextureWrap!.Width, _left.TextureWrap!.Height) - : (_right.TextureWrap!.Width, _right.TextureWrap!.Height); + (_targetWidth, _targetHeight) = resizeOp switch + { + ResizeOp.ToLeft => (_left.TextureWrap!.Width, _left.TextureWrap!.Height), + ResizeOp.ToRight => (_right.TextureWrap!.Width, _right.TextureWrap!.Height), + ResizeOp.None when usesLeft => (_left.TextureWrap!.Width, _left.TextureWrap!.Height), + _ => (_right.TextureWrap!.Width, _right.TextureWrap!.Height), + }; _centerStorage.RgbaPixels = new byte[_targetWidth * _targetHeight * 4]; _centerStorage.Type = TextureType.Bitmap; try From 598f3db06aada150dad03ff61314538e8ef19646 Mon Sep 17 00:00:00 2001 From: Exter-N Date: Tue, 29 Aug 2023 00:42:59 +0200 Subject: [PATCH 10/15] Textures: PR #327 feedback --- Penumbra/Import/Textures/CombinedTexture.Manipulation.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/Import/Textures/CombinedTexture.Manipulation.cs b/Penumbra/Import/Textures/CombinedTexture.Manipulation.cs index 57749471..eb8db121 100644 --- a/Penumbra/Import/Textures/CombinedTexture.Manipulation.cs +++ b/Penumbra/Import/Textures/CombinedTexture.Manipulation.cs @@ -277,7 +277,7 @@ public partial class CombinedTexture try { if (usesLeft) - _leftPixels = (resizeOp == ResizeOp.ToRight) ? ResizePixels(_left.RgbaPixels, _left.TextureWrap!.Width, _left.TextureWrap!.Height) : _left.RgbaPixels; + _leftPixels = ResizePixels(_left.RgbaPixels, _left.TextureWrap!.Width, _left.TextureWrap!.Height); if (usesRight) (_rightWidth, _rightHeight, _rightPixels) = (resizeOp == ResizeOp.ToLeft) ? (_targetWidth, _targetHeight, ResizePixels(_right.RgbaPixels, _right.TextureWrap!.Width, _right.TextureWrap!.Height)) From 848e4ff8a649292656be3cad613713a20d8b03ef Mon Sep 17 00:00:00 2001 From: Exter-N Date: Tue, 29 Aug 2023 03:25:54 +0200 Subject: [PATCH 11/15] Textures: Refactor resizing code --- .../Textures/CombinedTexture.Manipulation.cs | 168 +++++++++++------- 1 file changed, 99 insertions(+), 69 deletions(-) diff --git a/Penumbra/Import/Textures/CombinedTexture.Manipulation.cs b/Penumbra/Import/Textures/CombinedTexture.Manipulation.cs index eb8db121..3c0e8193 100644 --- a/Penumbra/Import/Textures/CombinedTexture.Manipulation.cs +++ b/Penumbra/Import/Textures/CombinedTexture.Manipulation.cs @@ -10,6 +10,7 @@ using Dalamud.Interface; using Penumbra.UI; using SixLabors.ImageSharp; using SixLabors.ImageSharp.Processing; +using System.Linq; namespace Penumbra.Import.Textures; @@ -29,9 +30,11 @@ public partial class CombinedTexture private enum ResizeOp { - None = 0, - ToLeft = 1, - ToRight = 2, + LeftOnly = -2, + RightOnly = -1, + None = 0, + ToLeft = 1, + ToRight = 2, } [Flags] @@ -43,6 +46,38 @@ public partial class CombinedTexture Alpha = 8, } + private readonly record struct RgbaPixelData(int Width, int Height, byte[] PixelData) + { + public static readonly RgbaPixelData Empty = new(0, 0, Array.Empty()); + + public (int Width, int Height) Size + => (Width, Height); + + public Image ToImage() + => Image.LoadPixelData(PixelData, Width, Height); + + public RgbaPixelData Resize((int Width, int Height) size) + { + if (Width == size.Width && Height == size.Height) + return this; + + var result = WithNewPixelData(size); + using (var image = ToImage()) + { + image.Mutate(ctx => ctx.Resize(size.Width, size.Height, KnownResamplers.Lanczos3)); + image.CopyPixelDataTo(result.PixelData); + } + + return result; + } + + public static RgbaPixelData WithNewPixelData((int Width, int Height) size) + => new(size.Width, size.Height, new byte[size.Width * size.Height * 4]); + + public static RgbaPixelData FromTexture(Texture texture) + => new(texture.TextureWrap!.Width, texture.TextureWrap!.Height, texture.RgbaPixels); + } + private Matrix4x4 _multiplierLeft = Matrix4x4.Identity; private Vector4 _constantLeft = Vector4.Zero; private Matrix4x4 _multiplierRight = Matrix4x4.Identity; @@ -53,12 +88,9 @@ public partial class CombinedTexture private ResizeOp _resizeOp = ResizeOp.None; private Channels _copyChannels = Channels.Red | Channels.Green | Channels.Blue | Channels.Alpha; - private int _rightWidth = 0; - private int _rightHeight = 0; - private int _targetWidth = 0; - private int _targetHeight = 0; - private byte[] _leftPixels = Array.Empty(); - private byte[] _rightPixels = Array.Empty(); + private RgbaPixelData _targetPixels = RgbaPixelData.Empty; + private RgbaPixelData _leftPixels = RgbaPixelData.Empty; + private RgbaPixelData _rightPixels = RgbaPixelData.Empty; private static readonly IReadOnlyList CombineOpLabels = new string[] { @@ -76,16 +108,16 @@ public partial class CombinedTexture "Replace some input channels with those from the overlay.\nUseful for Multi maps.", }; - private static (bool UsesLeft, bool UsesRight) GetCombineOpFlags(CombineOp combineOp) + private static ResizeOp GetActualResizeOp(ResizeOp resizeOp, CombineOp combineOp) => combineOp switch { - CombineOp.LeftCopy => (true, false), - CombineOp.LeftMultiply => (true, false), - CombineOp.RightCopy => (false, true), - CombineOp.RightMultiply => (false, true), - CombineOp.Over => (true, true), - CombineOp.Under => (true, true), - CombineOp.CopyChannels => (true, true), + CombineOp.LeftCopy => ResizeOp.LeftOnly, + CombineOp.LeftMultiply => ResizeOp.LeftOnly, + CombineOp.RightCopy => ResizeOp.RightOnly, + CombineOp.RightMultiply => ResizeOp.RightOnly, + CombineOp.Over => resizeOp, + CombineOp.Under => resizeOp, + CombineOp.CopyChannels => resizeOp, _ => throw new ArgumentException($"Invalid combine operation {combineOp}"), }; @@ -145,27 +177,27 @@ public partial class CombinedTexture } private Vector4 DataLeft(int offset) - => CappedVector(_leftPixels, offset, _multiplierLeft, _constantLeft); + => CappedVector(_leftPixels.PixelData, offset, _multiplierLeft, _constantLeft); private Vector4 DataRight(int offset) - => CappedVector(_rightPixels, offset, _multiplierRight, _constantRight); + => CappedVector(_rightPixels.PixelData, offset, _multiplierRight, _constantRight); private Vector4 DataRight(int x, int y) { x += _offsetX; y += _offsetY; - if (x < 0 || x >= _rightWidth || y < 0 || y >= _rightHeight) + if (x < 0 || x >= _rightPixels.Width || y < 0 || y >= _rightPixels.Height) return Vector4.Zero; - var offset = (y * _rightWidth + x) * 4; - return CappedVector(_rightPixels, offset, _multiplierRight, _constantRight); + var offset = (y * _rightPixels.Width + x) * 4; + return CappedVector(_rightPixels.PixelData, offset, _multiplierRight, _constantRight); } private void AddPixelsMultiplied(int y, ParallelLoopState _) { - for (var x = 0; x < _targetWidth; ++x) + for (var x = 0; x < _targetPixels.Width; ++x) { - var offset = (_targetWidth * y + x) * 4; + var offset = (_targetPixels.Width * y + x) * 4; var left = DataLeft(offset); var right = DataRight(x, y); var alpha = right.W + left.W * (1 - right.W); @@ -181,9 +213,9 @@ public partial class CombinedTexture private void ReverseAddPixelsMultiplied(int y, ParallelLoopState _) { - for (var x = 0; x < _targetWidth; ++x) + for (var x = 0; x < _targetPixels.Width; ++x) { - var offset = (_targetWidth * y + x) * 4; + var offset = (_targetPixels.Width * y + x) * 4; var left = DataLeft(offset); var right = DataRight(x, y); var alpha = left.W + right.W * (1 - left.W); @@ -200,9 +232,9 @@ public partial class CombinedTexture private void ChannelMergePixelsMultiplied(int y, ParallelLoopState _) { var channels = _copyChannels; - for (var x = 0; x < _targetWidth; ++x) + for (var x = 0; x < _targetPixels.Width; ++x) { - var offset = (_targetWidth * y + x) * 4; + var offset = (_targetPixels.Width * y + x) * 4; var left = DataLeft(offset); var right = DataRight(x, y); var rgba = new Rgba32((channels & Channels.Red) != 0 ? right.X : left.X, @@ -218,9 +250,9 @@ public partial class CombinedTexture private void MultiplyPixelsLeft(int y, ParallelLoopState _) { - for (var x = 0; x < _targetWidth; ++x) + for (var x = 0; x < _targetPixels.Width; ++x) { - var offset = (_targetWidth * y + x) * 4; + var offset = (_targetPixels.Width * y + x) * 4; var left = DataLeft(offset); var rgba = new Rgba32(left); _centerStorage.RgbaPixels[offset] = rgba.R; @@ -232,9 +264,9 @@ public partial class CombinedTexture private void MultiplyPixelsRight(int y, ParallelLoopState _) { - for (var x = 0; x < _targetWidth; ++x) + for (var x = 0; x < _targetPixels.Width; ++x) { - var offset = (_targetWidth * y + x) * 4; + var offset = (_targetPixels.Width * y + x) * 4; var right = DataRight(offset); var rgba = new Rgba32(right); _centerStorage.RgbaPixels[offset] = rgba.R; @@ -244,45 +276,42 @@ public partial class CombinedTexture } } - private byte[] ResizePixels(byte[] rgbaPixels, int sourceWidth, int sourceHeight) - { - if (sourceWidth == _targetWidth && sourceHeight == _targetHeight) - return rgbaPixels; - - var resizedPixels = new byte[_targetWidth * _targetHeight * 4]; - using (var image = Image.LoadPixelData(rgbaPixels, sourceWidth, sourceHeight)) - { - image.Mutate(ctx => ctx.Resize(_targetWidth, _targetHeight, KnownResamplers.Lanczos3)); - image.CopyPixelDataTo(resizedPixels); - } - - return resizedPixels; - } - private (int Width, int Height) CombineImage() { var combineOp = GetActualCombineOp(); - var (usesLeft, usesRight) = GetCombineOpFlags(combineOp); - var resizeOp = usesLeft && usesRight ? _resizeOp : ResizeOp.None; - (_targetWidth, _targetHeight) = resizeOp switch + var resizeOp = GetActualResizeOp(_resizeOp, combineOp); + + var left = resizeOp != ResizeOp.RightOnly ? RgbaPixelData.FromTexture(_left) : RgbaPixelData.Empty; + var right = resizeOp != ResizeOp.LeftOnly ? RgbaPixelData.FromTexture(_right) : RgbaPixelData.Empty; + + var targetSize = resizeOp switch { - ResizeOp.ToLeft => (_left.TextureWrap!.Width, _left.TextureWrap!.Height), - ResizeOp.ToRight => (_right.TextureWrap!.Width, _right.TextureWrap!.Height), - ResizeOp.None when usesLeft => (_left.TextureWrap!.Width, _left.TextureWrap!.Height), - _ => (_right.TextureWrap!.Width, _right.TextureWrap!.Height), + ResizeOp.RightOnly => right.Size, + ResizeOp.ToRight => right.Size, + _ => left.Size, }; - _centerStorage.RgbaPixels = new byte[_targetWidth * _targetHeight * 4]; - _centerStorage.Type = TextureType.Bitmap; + try { - if (usesLeft) - _leftPixels = ResizePixels(_left.RgbaPixels, _left.TextureWrap!.Width, _left.TextureWrap!.Height); - if (usesRight) - (_rightWidth, _rightHeight, _rightPixels) = (resizeOp == ResizeOp.ToLeft) - ? (_targetWidth, _targetHeight, ResizePixels(_right.RgbaPixels, _right.TextureWrap!.Width, _right.TextureWrap!.Height)) - : (_right.TextureWrap!.Width, _right.TextureWrap!.Height, _right.RgbaPixels); - Parallel.For(0, _targetHeight, combineOp switch + _targetPixels = RgbaPixelData.WithNewPixelData(targetSize); + + _centerStorage.RgbaPixels = _targetPixels.PixelData; + _centerStorage.Type = TextureType.Bitmap; + + _leftPixels = resizeOp switch + { + ResizeOp.RightOnly => RgbaPixelData.Empty, + _ => left.Resize(targetSize), + }; + _rightPixels = resizeOp switch + { + ResizeOp.LeftOnly => RgbaPixelData.Empty, + ResizeOp.None => right, + _ => right.Resize(targetSize), + }; + + Parallel.For(0, _targetPixels.Height, combineOp switch { CombineOp.Over => AddPixelsMultiplied, CombineOp.Under => ReverseAddPixelsMultiplied, @@ -294,11 +323,12 @@ public partial class CombinedTexture } finally { - _leftPixels = Array.Empty(); - _rightPixels = Array.Empty(); + _leftPixels = RgbaPixelData.Empty; + _rightPixels = RgbaPixelData.Empty; + _targetPixels = RgbaPixelData.Empty; } - return (_targetWidth, _targetHeight); + return targetSize; } private static Vector4 CappedVector(IReadOnlyList bytes, int offset, Matrix4x4 transform, Vector4 constant) @@ -367,11 +397,11 @@ public partial class CombinedTexture } } - var (usesLeft, usesRight) = GetCombineOpFlags(_combineOp); - using (var dis = ImRaii.Disabled(!usesLeft || !usesRight)) + var resizeOp = GetActualResizeOp(_resizeOp, _combineOp); + using (var dis = ImRaii.Disabled((int)resizeOp < 0)) { ret |= ImGuiUtil.GenericEnumCombo("Resizing Mode", 200.0f * UiHelpers.Scale, _resizeOp, out _resizeOp, - Enum.GetValues(), op => ResizeOpLabels[(int)op]); + Enum.GetValues().Where(op => (int)op >= 0), op => ResizeOpLabels[(int)op]); } using (var dis = ImRaii.Disabled(_combineOp != CombineOp.CopyChannels)) From 38a22c5298865b44180a9a5d2f77809de5ae30ac Mon Sep 17 00:00:00 2001 From: Exter-N Date: Wed, 30 Aug 2023 02:51:36 +0200 Subject: [PATCH 12/15] Textures: Simplify away _targetPixels --- .../Textures/CombinedTexture.Manipulation.cs | 43 ++++++++++--------- 1 file changed, 22 insertions(+), 21 deletions(-) diff --git a/Penumbra/Import/Textures/CombinedTexture.Manipulation.cs b/Penumbra/Import/Textures/CombinedTexture.Manipulation.cs index 3c0e8193..1633de62 100644 --- a/Penumbra/Import/Textures/CombinedTexture.Manipulation.cs +++ b/Penumbra/Import/Textures/CombinedTexture.Manipulation.cs @@ -53,6 +53,11 @@ public partial class CombinedTexture public (int Width, int Height) Size => (Width, Height); + public RgbaPixelData((int Width, int Height) size, byte[] pixelData) + : this(size.Width, size.Height, pixelData) + { + } + public Image ToImage() => Image.LoadPixelData(PixelData, Width, Height); @@ -61,7 +66,7 @@ public partial class CombinedTexture if (Width == size.Width && Height == size.Height) return this; - var result = WithNewPixelData(size); + var result = new RgbaPixelData(size, NewPixelData(size)); using (var image = ToImage()) { image.Mutate(ctx => ctx.Resize(size.Width, size.Height, KnownResamplers.Lanczos3)); @@ -71,8 +76,8 @@ public partial class CombinedTexture return result; } - public static RgbaPixelData WithNewPixelData((int Width, int Height) size) - => new(size.Width, size.Height, new byte[size.Width * size.Height * 4]); + public static byte[] NewPixelData((int Width, int Height) size) + => new byte[size.Width * size.Height * 4]; public static RgbaPixelData FromTexture(Texture texture) => new(texture.TextureWrap!.Width, texture.TextureWrap!.Height, texture.RgbaPixels); @@ -88,9 +93,8 @@ public partial class CombinedTexture private ResizeOp _resizeOp = ResizeOp.None; private Channels _copyChannels = Channels.Red | Channels.Green | Channels.Blue | Channels.Alpha; - private RgbaPixelData _targetPixels = RgbaPixelData.Empty; - private RgbaPixelData _leftPixels = RgbaPixelData.Empty; - private RgbaPixelData _rightPixels = RgbaPixelData.Empty; + private RgbaPixelData _leftPixels = RgbaPixelData.Empty; + private RgbaPixelData _rightPixels = RgbaPixelData.Empty; private static readonly IReadOnlyList CombineOpLabels = new string[] { @@ -195,9 +199,9 @@ public partial class CombinedTexture private void AddPixelsMultiplied(int y, ParallelLoopState _) { - for (var x = 0; x < _targetPixels.Width; ++x) + for (var x = 0; x < _leftPixels.Width; ++x) { - var offset = (_targetPixels.Width * y + x) * 4; + var offset = (_leftPixels.Width * y + x) * 4; var left = DataLeft(offset); var right = DataRight(x, y); var alpha = right.W + left.W * (1 - right.W); @@ -213,9 +217,9 @@ public partial class CombinedTexture private void ReverseAddPixelsMultiplied(int y, ParallelLoopState _) { - for (var x = 0; x < _targetPixels.Width; ++x) + for (var x = 0; x < _leftPixels.Width; ++x) { - var offset = (_targetPixels.Width * y + x) * 4; + var offset = (_leftPixels.Width * y + x) * 4; var left = DataLeft(offset); var right = DataRight(x, y); var alpha = left.W + right.W * (1 - left.W); @@ -232,9 +236,9 @@ public partial class CombinedTexture private void ChannelMergePixelsMultiplied(int y, ParallelLoopState _) { var channels = _copyChannels; - for (var x = 0; x < _targetPixels.Width; ++x) + for (var x = 0; x < _leftPixels.Width; ++x) { - var offset = (_targetPixels.Width * y + x) * 4; + var offset = (_leftPixels.Width * y + x) * 4; var left = DataLeft(offset); var right = DataRight(x, y); var rgba = new Rgba32((channels & Channels.Red) != 0 ? right.X : left.X, @@ -250,9 +254,9 @@ public partial class CombinedTexture private void MultiplyPixelsLeft(int y, ParallelLoopState _) { - for (var x = 0; x < _targetPixels.Width; ++x) + for (var x = 0; x < _leftPixels.Width; ++x) { - var offset = (_targetPixels.Width * y + x) * 4; + var offset = (_leftPixels.Width * y + x) * 4; var left = DataLeft(offset); var rgba = new Rgba32(left); _centerStorage.RgbaPixels[offset] = rgba.R; @@ -264,9 +268,9 @@ public partial class CombinedTexture private void MultiplyPixelsRight(int y, ParallelLoopState _) { - for (var x = 0; x < _targetPixels.Width; ++x) + for (var x = 0; x < _rightPixels.Width; ++x) { - var offset = (_targetPixels.Width * y + x) * 4; + var offset = (_rightPixels.Width * y + x) * 4; var right = DataRight(offset); var rgba = new Rgba32(right); _centerStorage.RgbaPixels[offset] = rgba.R; @@ -294,9 +298,7 @@ public partial class CombinedTexture try { - _targetPixels = RgbaPixelData.WithNewPixelData(targetSize); - - _centerStorage.RgbaPixels = _targetPixels.PixelData; + _centerStorage.RgbaPixels = RgbaPixelData.NewPixelData(targetSize); _centerStorage.Type = TextureType.Bitmap; _leftPixels = resizeOp switch @@ -311,7 +313,7 @@ public partial class CombinedTexture _ => right.Resize(targetSize), }; - Parallel.For(0, _targetPixels.Height, combineOp switch + Parallel.For(0, targetSize.Height, combineOp switch { CombineOp.Over => AddPixelsMultiplied, CombineOp.Under => ReverseAddPixelsMultiplied, @@ -325,7 +327,6 @@ public partial class CombinedTexture { _leftPixels = RgbaPixelData.Empty; _rightPixels = RgbaPixelData.Empty; - _targetPixels = RgbaPixelData.Empty; } return targetSize; From 600f5987cda4f9e345fdb17830d500acffd52f93 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 30 Aug 2023 17:25:26 +0200 Subject: [PATCH 13/15] Slight restructuring. --- .../Textures/CombinedTexture.Manipulation.cs | 230 ++---------------- .../Textures/CombinedTexture.Operations.cs | 150 ++++++++++++ Penumbra/Import/Textures/RgbaPixelData.cs | 43 ++++ 3 files changed, 219 insertions(+), 204 deletions(-) create mode 100644 Penumbra/Import/Textures/CombinedTexture.Operations.cs create mode 100644 Penumbra/Import/Textures/RgbaPixelData.cs diff --git a/Penumbra/Import/Textures/CombinedTexture.Manipulation.cs b/Penumbra/Import/Textures/CombinedTexture.Manipulation.cs index 1633de62..9bc4a2a5 100644 --- a/Penumbra/Import/Textures/CombinedTexture.Manipulation.cs +++ b/Penumbra/Import/Textures/CombinedTexture.Manipulation.cs @@ -8,81 +8,12 @@ using OtterGui; using SixLabors.ImageSharp.PixelFormats; using Dalamud.Interface; using Penumbra.UI; -using SixLabors.ImageSharp; -using SixLabors.ImageSharp.Processing; using System.Linq; namespace Penumbra.Import.Textures; public partial class CombinedTexture { - private enum CombineOp - { - LeftMultiply = -4, - LeftCopy = -3, - RightCopy = -2, - Invalid = -1, - Over = 0, - Under = 1, - RightMultiply = 2, - CopyChannels = 3, - } - - private enum ResizeOp - { - LeftOnly = -2, - RightOnly = -1, - None = 0, - ToLeft = 1, - ToRight = 2, - } - - [Flags] - private enum Channels - { - Red = 1, - Green = 2, - Blue = 4, - Alpha = 8, - } - - private readonly record struct RgbaPixelData(int Width, int Height, byte[] PixelData) - { - public static readonly RgbaPixelData Empty = new(0, 0, Array.Empty()); - - public (int Width, int Height) Size - => (Width, Height); - - public RgbaPixelData((int Width, int Height) size, byte[] pixelData) - : this(size.Width, size.Height, pixelData) - { - } - - public Image ToImage() - => Image.LoadPixelData(PixelData, Width, Height); - - public RgbaPixelData Resize((int Width, int Height) size) - { - if (Width == size.Width && Height == size.Height) - return this; - - var result = new RgbaPixelData(size, NewPixelData(size)); - using (var image = ToImage()) - { - image.Mutate(ctx => ctx.Resize(size.Width, size.Height, KnownResamplers.Lanczos3)); - image.CopyPixelDataTo(result.PixelData); - } - - return result; - } - - public static byte[] NewPixelData((int Width, int Height) size) - => new byte[size.Width * size.Height * 4]; - - public static RgbaPixelData FromTexture(Texture texture) - => new(texture.TextureWrap!.Width, texture.TextureWrap!.Height, texture.RgbaPixels); - } - private Matrix4x4 _multiplierLeft = Matrix4x4.Identity; private Vector4 _constantLeft = Vector4.Zero; private Matrix4x4 _multiplierRight = Matrix4x4.Identity; @@ -96,42 +27,6 @@ public partial class CombinedTexture private RgbaPixelData _leftPixels = RgbaPixelData.Empty; private RgbaPixelData _rightPixels = RgbaPixelData.Empty; - private static readonly IReadOnlyList CombineOpLabels = new string[] - { - "Overlay over Input", - "Input over Overlay", - "Replace Input", - "Copy Channels", - }; - - private static readonly IReadOnlyList CombineOpTooltips = new string[] - { - "Standard composition.\nApply the overlay over the input.", - "Standard composition, reversed.\nApply the input over the overlay ; can be used to fix some wrong imports.", - "Completely replace the input with the overlay.\nCan be used to select the destination file as input and the source file as overlay.", - "Replace some input channels with those from the overlay.\nUseful for Multi maps.", - }; - - private static ResizeOp GetActualResizeOp(ResizeOp resizeOp, CombineOp combineOp) - => combineOp switch - { - CombineOp.LeftCopy => ResizeOp.LeftOnly, - CombineOp.LeftMultiply => ResizeOp.LeftOnly, - CombineOp.RightCopy => ResizeOp.RightOnly, - CombineOp.RightMultiply => ResizeOp.RightOnly, - CombineOp.Over => resizeOp, - CombineOp.Under => resizeOp, - CombineOp.CopyChannels => resizeOp, - _ => throw new ArgumentException($"Invalid combine operation {combineOp}"), - }; - - private static readonly IReadOnlyList ResizeOpLabels = new string[] - { - "No Resizing", - "Adjust Overlay to Input", - "Adjust Input to Overlay", - }; - private const float OneThird = 1.0f / 3.0f; private const float RWeight = 0.2126f; private const float GWeight = 0.7152f; @@ -154,32 +49,6 @@ public partial class CombinedTexture }; // @formatter:on - private CombineOp GetActualCombineOp() - { - var combineOp = (_left.IsLoaded, _right.IsLoaded) switch - { - (true, true) => _combineOp, - (true, false) => CombineOp.LeftMultiply, - (false, true) => CombineOp.RightMultiply, - (false, false) => CombineOp.Invalid, - }; - - if (combineOp == CombineOp.CopyChannels) - { - if (_copyChannels == 0) - combineOp = CombineOp.LeftMultiply; - else if (_copyChannels == (Channels.Red | Channels.Green | Channels.Blue | Channels.Alpha)) - combineOp = CombineOp.RightMultiply; - } - - return combineOp switch - { - CombineOp.LeftMultiply when _multiplierLeft.IsIdentity && _constantLeft == Vector4.Zero => CombineOp.LeftCopy, - CombineOp.RightMultiply when _multiplierRight.IsIdentity && _constantRight == Vector4.Zero => CombineOp.RightCopy, - _ => combineOp, - }; - } - private Vector4 DataLeft(int offset) => CappedVector(_leftPixels.PixelData, offset, _multiplierLeft, _constantLeft); @@ -280,11 +149,10 @@ public partial class CombinedTexture } } - private (int Width, int Height) CombineImage() { var combineOp = GetActualCombineOp(); - var resizeOp = GetActualResizeOp(_resizeOp, combineOp); + var resizeOp = GetActualResizeOp(_resizeOp, combineOp); var left = resizeOp != ResizeOp.RightOnly ? RgbaPixelData.FromTexture(_left) : RgbaPixelData.Empty; var right = resizeOp != ResizeOp.LeftOnly ? RgbaPixelData.FromTexture(_right) : RgbaPixelData.Empty; @@ -325,8 +193,8 @@ public partial class CombinedTexture } finally { - _leftPixels = RgbaPixelData.Empty; - _rightPixels = RgbaPixelData.Empty; + _leftPixels = RgbaPixelData.Empty; + _rightPixels = RgbaPixelData.Empty; } return targetSize; @@ -488,99 +356,53 @@ public partial class CombinedTexture private static bool DrawMatrixTools(ref Matrix4x4 multiplier, ref Vector4 constant) { - var changes = false; - - using (var combo = ImRaii.Combo("Presets", string.Empty, ImGuiComboFlags.NoPreview)) - { - if (combo) - foreach (var (label, preMultiplier, preConstant) in PredefinedColorTransforms) - { - if (ImGui.Selectable(label, multiplier == preMultiplier && constant == preConstant)) - { - multiplier = preMultiplier; - constant = preConstant; - changes = true; - } - } - } - + var changes = PresetCombo(ref multiplier, ref constant); ImGui.SameLine(); ImGui.Dummy(ImGuiHelpers.ScaledVector2(20, 0)); ImGui.SameLine(); ImGui.TextUnformatted("Invert"); ImGui.SameLine(); - if (ImGui.Button("Colors")) - { - InvertRed(ref multiplier, ref constant); - InvertGreen(ref multiplier, ref constant); - InvertBlue(ref multiplier, ref constant); - changes = true; - } + Channels channels = 0; + if (ImGui.Button("Colors")) + channels |= Channels.Red | Channels.Green | Channels.Blue; ImGui.SameLine(); if (ImGui.Button("R")) - { - InvertRed(ref multiplier, ref constant); - changes = true; - } + channels |= Channels.Red; ImGui.SameLine(); if (ImGui.Button("G")) - { - InvertGreen(ref multiplier, ref constant); - changes = true; - } + channels |= Channels.Green; ImGui.SameLine(); if (ImGui.Button("B")) - { - InvertBlue(ref multiplier, ref constant); - changes = true; - } + channels |= Channels.Blue; ImGui.SameLine(); if (ImGui.Button("A")) - { - InvertAlpha(ref multiplier, ref constant); - changes = true; - } + channels |= Channels.Alpha; + changes |= InvertChannels(channels, ref multiplier, ref constant); return changes; } - private static void InvertRed(ref Matrix4x4 multiplier, ref Vector4 constant) + private static bool PresetCombo(ref Matrix4x4 multiplier, ref Vector4 constant) { - multiplier.M11 = -multiplier.M11; - multiplier.M21 = -multiplier.M21; - multiplier.M31 = -multiplier.M31; - multiplier.M41 = -multiplier.M41; - constant.X = 1.0f - constant.X; - } + using var combo = ImRaii.Combo("Presets", string.Empty, ImGuiComboFlags.NoPreview); + if (!combo) + return false; - private static void InvertGreen(ref Matrix4x4 multiplier, ref Vector4 constant) - { - multiplier.M12 = -multiplier.M12; - multiplier.M22 = -multiplier.M22; - multiplier.M32 = -multiplier.M32; - multiplier.M42 = -multiplier.M42; - constant.Y = 1.0f - constant.Y; - } + var ret = false; + foreach (var (label, preMultiplier, preConstant) in PredefinedColorTransforms) + { + if (!ImGui.Selectable(label, multiplier == preMultiplier && constant == preConstant)) + continue; - private static void InvertBlue(ref Matrix4x4 multiplier, ref Vector4 constant) - { - multiplier.M13 = -multiplier.M13; - multiplier.M23 = -multiplier.M23; - multiplier.M33 = -multiplier.M33; - multiplier.M43 = -multiplier.M43; - constant.Z = 1.0f - constant.Z; - } + multiplier = preMultiplier; + constant = preConstant; + ret = true; + } - private static void InvertAlpha(ref Matrix4x4 multiplier, ref Vector4 constant) - { - multiplier.M14 = -multiplier.M14; - multiplier.M24 = -multiplier.M24; - multiplier.M34 = -multiplier.M34; - multiplier.M44 = -multiplier.M44; - constant.W = 1.0f - constant.W; + return ret; } } diff --git a/Penumbra/Import/Textures/CombinedTexture.Operations.cs b/Penumbra/Import/Textures/CombinedTexture.Operations.cs new file mode 100644 index 00000000..441cd3f0 --- /dev/null +++ b/Penumbra/Import/Textures/CombinedTexture.Operations.cs @@ -0,0 +1,150 @@ +using System; +using System.Collections.Generic; +using System.Numerics; + +namespace Penumbra.Import.Textures; + +public partial class CombinedTexture +{ + private enum CombineOp + { + LeftMultiply = -4, + LeftCopy = -3, + RightCopy = -2, + Invalid = -1, + Over = 0, + Under = 1, + RightMultiply = 2, + CopyChannels = 3, + } + + private enum ResizeOp + { + LeftOnly = -2, + RightOnly = -1, + None = 0, + ToLeft = 1, + ToRight = 2, + } + + [Flags] + private enum Channels : byte + { + Red = 1, + Green = 2, + Blue = 4, + Alpha = 8, + } + + private static readonly IReadOnlyList CombineOpLabels = new[] + { + "Overlay over Input", + "Input over Overlay", + "Replace Input", + "Copy Channels", + }; + + private static readonly IReadOnlyList CombineOpTooltips = new[] + { + "Standard composition.\nApply the overlay over the input.", + "Standard composition, reversed.\nApply the input over the overlay ; can be used to fix some wrong imports.", + "Completely replace the input with the overlay.\nCan be used to select the destination file as input and the source file as overlay.", + "Replace some input channels with those from the overlay.\nUseful for Multi maps.", + }; + + private static readonly IReadOnlyList ResizeOpLabels = new string[] + { + "No Resizing", + "Adjust Overlay to Input", + "Adjust Input to Overlay", + }; + + private static ResizeOp GetActualResizeOp(ResizeOp resizeOp, CombineOp combineOp) + => combineOp switch + { + CombineOp.LeftCopy => ResizeOp.LeftOnly, + CombineOp.LeftMultiply => ResizeOp.LeftOnly, + CombineOp.RightCopy => ResizeOp.RightOnly, + CombineOp.RightMultiply => ResizeOp.RightOnly, + CombineOp.Over => resizeOp, + CombineOp.Under => resizeOp, + CombineOp.CopyChannels => resizeOp, + _ => throw new ArgumentException($"Invalid combine operation {combineOp}"), + }; + + private CombineOp GetActualCombineOp() + { + var combineOp = (_left.IsLoaded, _right.IsLoaded) switch + { + (true, true) => _combineOp, + (true, false) => CombineOp.LeftMultiply, + (false, true) => CombineOp.RightMultiply, + (false, false) => CombineOp.Invalid, + }; + + if (combineOp == CombineOp.CopyChannels) + { + if (_copyChannels == 0) + combineOp = CombineOp.LeftMultiply; + else if (_copyChannels == (Channels.Red | Channels.Green | Channels.Blue | Channels.Alpha)) + combineOp = CombineOp.RightMultiply; + } + + return combineOp switch + { + CombineOp.LeftMultiply when _multiplierLeft.IsIdentity && _constantLeft == Vector4.Zero => CombineOp.LeftCopy, + CombineOp.RightMultiply when _multiplierRight.IsIdentity && _constantRight == Vector4.Zero => CombineOp.RightCopy, + _ => combineOp, + }; + } + + + private static bool InvertChannels(Channels channels, ref Matrix4x4 multiplier, ref Vector4 constant) + { + if (channels.HasFlag(Channels.Red)) + InvertRed(ref multiplier, ref constant); + if (channels.HasFlag(Channels.Green)) + InvertGreen(ref multiplier, ref constant); + if (channels.HasFlag(Channels.Blue)) + InvertBlue(ref multiplier, ref constant); + if (channels.HasFlag(Channels.Alpha)) + InvertAlpha(ref multiplier, ref constant); + return channels != 0; + } + + private static void InvertRed(ref Matrix4x4 multiplier, ref Vector4 constant) + { + multiplier.M11 = -multiplier.M11; + multiplier.M21 = -multiplier.M21; + multiplier.M31 = -multiplier.M31; + multiplier.M41 = -multiplier.M41; + constant.X = 1.0f - constant.X; + } + + private static void InvertGreen(ref Matrix4x4 multiplier, ref Vector4 constant) + { + multiplier.M12 = -multiplier.M12; + multiplier.M22 = -multiplier.M22; + multiplier.M32 = -multiplier.M32; + multiplier.M42 = -multiplier.M42; + constant.Y = 1.0f - constant.Y; + } + + private static void InvertBlue(ref Matrix4x4 multiplier, ref Vector4 constant) + { + multiplier.M13 = -multiplier.M13; + multiplier.M23 = -multiplier.M23; + multiplier.M33 = -multiplier.M33; + multiplier.M43 = -multiplier.M43; + constant.Z = 1.0f - constant.Z; + } + + private static void InvertAlpha(ref Matrix4x4 multiplier, ref Vector4 constant) + { + multiplier.M14 = -multiplier.M14; + multiplier.M24 = -multiplier.M24; + multiplier.M34 = -multiplier.M34; + multiplier.M44 = -multiplier.M44; + constant.W = 1.0f - constant.W; + } +} diff --git a/Penumbra/Import/Textures/RgbaPixelData.cs b/Penumbra/Import/Textures/RgbaPixelData.cs new file mode 100644 index 00000000..0314b104 --- /dev/null +++ b/Penumbra/Import/Textures/RgbaPixelData.cs @@ -0,0 +1,43 @@ +using System; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; + +namespace Penumbra.Import.Textures; + +public readonly record struct RgbaPixelData(int Width, int Height, byte[] PixelData) +{ + public static readonly RgbaPixelData Empty = new(0, 0, Array.Empty()); + + public (int Width, int Height) Size + => (Width, Height); + + public RgbaPixelData((int Width, int Height) size, byte[] pixelData) + : this(size.Width, size.Height, pixelData) + { + } + + public Image ToImage() + => Image.LoadPixelData(PixelData, Width, Height); + + public RgbaPixelData Resize((int Width, int Height) size) + { + if (Width == size.Width && Height == size.Height) + return this; + + var result = new RgbaPixelData(size, NewPixelData(size)); + using (var image = ToImage()) + { + image.Mutate(ctx => ctx.Resize(size.Width, size.Height, KnownResamplers.Lanczos3)); + image.CopyPixelDataTo(result.PixelData); + } + + return result; + } + + public static byte[] NewPixelData((int Width, int Height) size) + => new byte[size.Width * size.Height * 4]; + + public static RgbaPixelData FromTexture(Texture texture) + => new(texture.TextureWrap!.Width, texture.TextureWrap!.Height, texture.RgbaPixels); +} From f23804975004b9841e99a3f77aa8149425be9581 Mon Sep 17 00:00:00 2001 From: Exter-N Date: Wed, 30 Aug 2023 19:16:22 +0200 Subject: [PATCH 14/15] Skin Fixer: Fix potential ref leak + add SRH `SafeResourceHandle` wraps a `ResourceHandle*` with auto `IncRef` / `DecRef`, to further help prevent leaks. --- .../Interop/SafeHandles/SafeResourceHandle.cs | 34 ++++++++++++++++ Penumbra/Interop/Services/SkinFixer.cs | 40 ++++++++++++------- 2 files changed, 60 insertions(+), 14 deletions(-) create mode 100644 Penumbra/Interop/SafeHandles/SafeResourceHandle.cs diff --git a/Penumbra/Interop/SafeHandles/SafeResourceHandle.cs b/Penumbra/Interop/SafeHandles/SafeResourceHandle.cs new file mode 100644 index 00000000..7ec0f218 --- /dev/null +++ b/Penumbra/Interop/SafeHandles/SafeResourceHandle.cs @@ -0,0 +1,34 @@ +using System; +using System.Runtime.InteropServices; +using System.Threading; +using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; + +namespace Penumbra.Interop.SafeHandles; + +public unsafe class SafeResourceHandle : SafeHandle +{ + public ResourceHandle* ResourceHandle => (ResourceHandle*)handle; + + public override bool IsInvalid => handle == 0; + + public SafeResourceHandle(ResourceHandle* handle, bool incRef, bool ownsHandle = true) : base(0, ownsHandle) + { + if (incRef && !ownsHandle) + throw new ArgumentException("Non-owning SafeResourceHandle with IncRef is unsupported"); + if (incRef && handle != null) + handle->IncRef(); + SetHandle((nint)handle); + } + + public static SafeResourceHandle CreateInvalid() + => new(null, incRef: false); + + protected override bool ReleaseHandle() + { + var handle = Interlocked.Exchange(ref this.handle, 0); + if (handle != 0) + ((ResourceHandle*)handle)->DecRef(); + + return true; + } +} diff --git a/Penumbra/Interop/Services/SkinFixer.cs b/Penumbra/Interop/Services/SkinFixer.cs index 479feafc..d72cedfb 100644 --- a/Penumbra/Interop/Services/SkinFixer.cs +++ b/Penumbra/Interop/Services/SkinFixer.cs @@ -14,6 +14,7 @@ using Penumbra.GameData; using Penumbra.GameData.Enums; using Penumbra.Interop.PathResolving; using Penumbra.Interop.ResourceLoading; +using Penumbra.Interop.SafeHandles; using Penumbra.String.Classes; namespace Penumbra.Interop.Services; @@ -44,7 +45,7 @@ public unsafe class SkinFixer : IDisposable private readonly ResourceLoader _resources; private readonly CharacterUtility _utility; - private readonly ConcurrentDictionary _skinShpks = new(); + private readonly ConcurrentDictionary _skinShpks = new(); private readonly object _lock = new(); @@ -89,8 +90,7 @@ public unsafe class SkinFixer : IDisposable _gameEvents.CharacterBaseCreated -= OnCharacterBaseCreated; _gameEvents.CharacterBaseDestructor -= OnCharacterBaseDestructor; foreach (var skinShpk in _skinShpks.Values) - if (skinShpk != nint.Zero) - ((ResourceHandle*)skinShpk)->DecRef(); + skinShpk.Dispose(); _skinShpks.Clear(); _moddedSkinShpkCount = 0; } @@ -105,29 +105,41 @@ public unsafe class SkinFixer : IDisposable Task.Run(delegate { - nint skinShpk; + var skinShpk = SafeResourceHandle.CreateInvalid(); try { var data = _collectionResolver.IdentifyCollection((DrawObject*)drawObject, true); - skinShpk = data.Valid ? (nint)_resources.LoadResolvedResource(ResourceCategory.Shader, ResourceType.Shpk, SkinShpkPath.Path, data) : nint.Zero; + if (data.Valid) + { + var loadedShpk = _resources.LoadResolvedResource(ResourceCategory.Shader, ResourceType.Shpk, SkinShpkPath.Path, data); + skinShpk = new SafeResourceHandle((ResourceHandle*)loadedShpk, incRef: false); + } } catch (Exception e) { Penumbra.Log.Error($"Error while resolving skin.shpk for human {drawObject:X}: {e}"); - skinShpk = nint.Zero; } - if (skinShpk != nint.Zero && _skinShpks.TryAdd(drawObject, skinShpk) && skinShpk != _utility.DefaultSkinShpkResource) - Interlocked.Increment(ref _moddedSkinShpkCount); + if (!skinShpk.IsInvalid) + { + if (_skinShpks.TryAdd(drawObject, skinShpk)) + { + if ((nint)skinShpk.ResourceHandle != _utility.DefaultSkinShpkResource) + Interlocked.Increment(ref _moddedSkinShpkCount); + } + else + skinShpk.Dispose(); + } }); } private void OnCharacterBaseDestructor(nint characterBase) { - if (_skinShpks.Remove(characterBase, out var skinShpk) && skinShpk != nint.Zero) + if (_skinShpks.Remove(characterBase, out var skinShpk)) { - ((ResourceHandle*)skinShpk)->DecRef(); - if (skinShpk != _utility.DefaultSkinShpkResource) + var handle = skinShpk.ResourceHandle; + skinShpk.Dispose(); + if ((nint)handle != _utility.DefaultSkinShpkResource) Interlocked.Decrement(ref _moddedSkinShpkCount); } } @@ -136,12 +148,12 @@ public unsafe class SkinFixer : IDisposable { if (!_enabled || // Can be toggled on the debug tab. _moddedSkinShpkCount == 0 || // If we don't have any on-screen instances of modded skin.shpk, we don't need the slow path at all. - !_skinShpks.TryGetValue(human, out var skinShpk) || skinShpk == nint.Zero) + !_skinShpks.TryGetValue(human, out var skinShpk)) return _onRenderMaterialHook!.Original(human, param); var material = param->Model->Materials[param->MaterialIndex]; var shpkResource = ((Structs.MtrlResource*)material->MaterialResourceHandle)->ShpkResourceHandle; - if ((nint)shpkResource != skinShpk) + if ((nint)shpkResource != (nint)skinShpk.ResourceHandle) return _onRenderMaterialHook!.Original(human, param); Interlocked.Increment(ref _slowPathCallDelta); @@ -154,7 +166,7 @@ public unsafe class SkinFixer : IDisposable lock (_lock) try { - _utility.Address->SkinShpkResource = (Structs.ResourceHandle*)skinShpk; + _utility.Address->SkinShpkResource = (Structs.ResourceHandle*)skinShpk.ResourceHandle; return _onRenderMaterialHook!.Original(human, param); } finally From 6d3e93044072addf83169dde3f2c73433c5e2141 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 30 Aug 2023 20:52:21 +0200 Subject: [PATCH 15/15] Use better event in SkinFixer and some cleanup. --- Penumbra/Api/PenumbraApi.cs | 26 ++---- .../Communication/CreatedCharacterBase.cs | 14 ++-- Penumbra/Interop/PathResolving/MetaState.cs | 2 +- Penumbra/Interop/Services/SkinFixer.cs | 83 +++++++++---------- Penumbra/UI/Tabs/DebugTab.cs | 11 +-- 5 files changed, 57 insertions(+), 79 deletions(-) diff --git a/Penumbra/Api/PenumbraApi.cs b/Penumbra/Api/PenumbraApi.cs index 01078450..9d578190 100644 --- a/Penumbra/Api/PenumbraApi.cs +++ b/Penumbra/Api/PenumbraApi.cs @@ -85,26 +85,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi } } - public event CreatedCharacterBaseDelegate? CreatedCharacterBase - { - add - { - if (value == null) - return; - - CheckInitialized(); - _communicator.CreatedCharacterBase.Subscribe(new Action(value), - Communication.CreatedCharacterBase.Priority.Api); - } - remove - { - if (value == null) - return; - - CheckInitialized(); - _communicator.CreatedCharacterBase.Unsubscribe(new Action(value)); - } - } + public event CreatedCharacterBaseDelegate? CreatedCharacterBase; public bool Valid => _lumina != null; @@ -157,6 +138,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi _resourceLoader.ResourceLoaded += OnResourceLoaded; _communicator.ModPathChanged.Subscribe(ModPathChangeSubscriber, ModPathChanged.Priority.Api); _communicator.ModSettingChanged.Subscribe(OnModSettingChange, Communication.ModSettingChanged.Priority.Api); + _communicator.CreatedCharacterBase.Subscribe(OnCreatedCharacterBase, Communication.CreatedCharacterBase.Priority.Api); } public unsafe void Dispose() @@ -167,6 +149,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi _resourceLoader.ResourceLoaded -= OnResourceLoaded; _communicator.ModPathChanged.Unsubscribe(ModPathChangeSubscriber); _communicator.ModSettingChanged.Unsubscribe(OnModSettingChange); + _communicator.CreatedCharacterBase.Unsubscribe(OnCreatedCharacterBase); _lumina = null; _communicator = null!; _modManager = null!; @@ -1189,4 +1172,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi private void OnModSettingChange(ModCollection collection, ModSettingChange type, Mod? mod, int _1, int _2, bool inherited) => ModSettingChanged?.Invoke(type, collection.Name, mod?.ModPath.Name ?? string.Empty, inherited); + + private void OnCreatedCharacterBase(nint gameObject, ModCollection collection, nint drawObject) + => CreatedCharacterBase?.Invoke(gameObject, collection.Name, drawObject); } diff --git a/Penumbra/Communication/CreatedCharacterBase.cs b/Penumbra/Communication/CreatedCharacterBase.cs index cbb86fc2..48ba86a5 100644 --- a/Penumbra/Communication/CreatedCharacterBase.cs +++ b/Penumbra/Communication/CreatedCharacterBase.cs @@ -1,26 +1,30 @@ using System; using OtterGui.Classes; using Penumbra.Api; +using Penumbra.Collections; namespace Penumbra.Communication; /// /// Parameter is the game object for which a draw object is created. -/// Parameter is the name of the applied collection. +/// Parameter is the applied collection. /// Parameter is the created draw object. /// -public sealed class CreatedCharacterBase : EventWrapper, CreatedCharacterBase.Priority> +public sealed class CreatedCharacterBase : EventWrapper, CreatedCharacterBase.Priority> { public enum Priority { /// - Api = 0, + Api = int.MinValue, + + /// + SkinFixer = 0, } public CreatedCharacterBase() : base(nameof(CreatedCharacterBase)) { } - public void Invoke(nint gameObject, string appliedCollectionName, nint drawObject) - => Invoke(this, gameObject, appliedCollectionName, drawObject); + public void Invoke(nint gameObject, ModCollection appliedCollection, nint drawObject) + => Invoke(this, gameObject, appliedCollection, drawObject); } diff --git a/Penumbra/Interop/PathResolving/MetaState.cs b/Penumbra/Interop/PathResolving/MetaState.cs index a4cbc967..1a257a96 100644 --- a/Penumbra/Interop/PathResolving/MetaState.cs +++ b/Penumbra/Interop/PathResolving/MetaState.cs @@ -142,7 +142,7 @@ public unsafe class MetaState : IDisposable _characterBaseCreateMetaChanges = DisposableContainer.Empty; if (_lastCreatedCollection.Valid && _lastCreatedCollection.AssociatedGameObject != nint.Zero) _communicator.CreatedCharacterBase.Invoke(_lastCreatedCollection.AssociatedGameObject, - _lastCreatedCollection.ModCollection.Name, drawObject); + _lastCreatedCollection.ModCollection, drawObject); _lastCreatedCollection = ResolveData.Invalid; } diff --git a/Penumbra/Interop/Services/SkinFixer.cs b/Penumbra/Interop/Services/SkinFixer.cs index d72cedfb..be45708f 100644 --- a/Penumbra/Interop/Services/SkinFixer.cs +++ b/Penumbra/Interop/Services/SkinFixer.cs @@ -10,16 +10,18 @@ using FFXIVClientStructs.FFXIV.Client.Graphics.Render; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using FFXIVClientStructs.FFXIV.Client.System.Resource; using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; +using Penumbra.Collections; +using Penumbra.Communication; using Penumbra.GameData; using Penumbra.GameData.Enums; -using Penumbra.Interop.PathResolving; using Penumbra.Interop.ResourceLoading; using Penumbra.Interop.SafeHandles; +using Penumbra.Services; using Penumbra.String.Classes; namespace Penumbra.Interop.Services; -public unsafe class SkinFixer : IDisposable +public sealed unsafe class SkinFixer : IDisposable { public static readonly Utf8GamePath SkinShpkPath = Utf8GamePath.FromSpan("shader/sm5/shpk/skin.shpk"u8, out var p) ? p : Utf8GamePath.Empty; @@ -34,60 +36,48 @@ public unsafe class SkinFixer : IDisposable { [FieldOffset(0x0)] public Model* Model; + [FieldOffset(0x8)] public uint MaterialIndex; } private readonly Hook _onRenderMaterialHook; - private readonly CollectionResolver _collectionResolver; - private readonly GameEventManager _gameEvents; - private readonly ResourceLoader _resources; - private readonly CharacterUtility _utility; - - private readonly ConcurrentDictionary _skinShpks = new(); + private readonly GameEventManager _gameEvents; + private readonly CommunicatorService _communicator; + private readonly ResourceLoader _resources; + private readonly CharacterUtility _utility; + + // CharacterBase to ShpkHandle + private readonly ConcurrentDictionary _skinShpks = new(); private readonly object _lock = new(); - - private bool _enabled = true; + private int _moddedSkinShpkCount = 0; - private ulong _slowPathCallDelta = 0; - public bool Enabled - { - get => _enabled; - set => _enabled = value; - } + private ulong _slowPathCallDelta = 0; + + public bool Enabled { get; internal set; } = true; public int ModdedSkinShpkCount => _moddedSkinShpkCount; - public SkinFixer(CollectionResolver collectionResolver, GameEventManager gameEvents, ResourceLoader resources, CharacterUtility utility, DrawObjectState _) + public SkinFixer(GameEventManager gameEvents, ResourceLoader resources, CharacterUtility utility, CommunicatorService communicator) { SignatureHelper.Initialise(this); - _collectionResolver = collectionResolver; _gameEvents = gameEvents; _resources = resources; _utility = utility; + _communicator = communicator; _onRenderMaterialHook = Hook.FromAddress(_humanVTable[62], OnRenderHumanMaterial); - _gameEvents.CharacterBaseCreated += OnCharacterBaseCreated; // The dependency on DrawObjectState shall ensure that this handler is registered after its one. + _communicator.CreatedCharacterBase.Subscribe(OnCharacterBaseCreated, CreatedCharacterBase.Priority.SkinFixer); _gameEvents.CharacterBaseDestructor += OnCharacterBaseDestructor; _onRenderMaterialHook.Enable(); } - ~SkinFixer() - { - Dispose(false); - } - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - protected virtual void Dispose(bool disposing) { _onRenderMaterialHook.Dispose(); - _gameEvents.CharacterBaseCreated -= OnCharacterBaseCreated; + _communicator.CreatedCharacterBase.Unsubscribe(OnCharacterBaseCreated); _gameEvents.CharacterBaseDestructor -= OnCharacterBaseDestructor; foreach (var skinShpk in _skinShpks.Values) skinShpk.Dispose(); @@ -98,21 +88,21 @@ public unsafe class SkinFixer : IDisposable public ulong GetAndResetSlowPathCallDelta() => Interlocked.Exchange(ref _slowPathCallDelta, 0); - private void OnCharacterBaseCreated(uint modelCharaId, nint customize, nint equipment, nint drawObject) + private void OnCharacterBaseCreated(nint gameObject, ModCollection collection, nint drawObject) { if (((CharacterBase*)drawObject)->GetModelType() != CharacterBase.ModelType.Human) return; - Task.Run(delegate + Task.Run(() => { var skinShpk = SafeResourceHandle.CreateInvalid(); try { - var data = _collectionResolver.IdentifyCollection((DrawObject*)drawObject, true); + var data = collection.ToResolveData(gameObject); if (data.Valid) { var loadedShpk = _resources.LoadResolvedResource(ResourceCategory.Shader, ResourceType.Shpk, SkinShpkPath.Path, data); - skinShpk = new SafeResourceHandle((ResourceHandle*)loadedShpk, incRef: false); + skinShpk = new SafeResourceHandle((ResourceHandle*)loadedShpk, false); } } catch (Exception e) @@ -128,30 +118,31 @@ public unsafe class SkinFixer : IDisposable Interlocked.Increment(ref _moddedSkinShpkCount); } else + { skinShpk.Dispose(); + } } }); } private void OnCharacterBaseDestructor(nint characterBase) { - if (_skinShpks.Remove(characterBase, out var skinShpk)) - { - var handle = skinShpk.ResourceHandle; - skinShpk.Dispose(); - if ((nint)handle != _utility.DefaultSkinShpkResource) - Interlocked.Decrement(ref _moddedSkinShpkCount); - } + if (!_skinShpks.Remove(characterBase, out var skinShpk)) + return; + + var handle = skinShpk.ResourceHandle; + skinShpk.Dispose(); + if ((nint)handle != _utility.DefaultSkinShpkResource) + Interlocked.Decrement(ref _moddedSkinShpkCount); } private nint OnRenderHumanMaterial(nint human, OnRenderMaterialParams* param) { - if (!_enabled || // Can be toggled on the debug tab. - _moddedSkinShpkCount == 0 || // If we don't have any on-screen instances of modded skin.shpk, we don't need the slow path at all. - !_skinShpks.TryGetValue(human, out var skinShpk)) + // If we don't have any on-screen instances of modded skin.shpk, we don't need the slow path at all. + if (!Enabled || _moddedSkinShpkCount == 0 || !_skinShpks.TryGetValue(human, out var skinShpk) || skinShpk.IsInvalid) return _onRenderMaterialHook!.Original(human, param); - var material = param->Model->Materials[param->MaterialIndex]; + var material = param->Model->Materials[param->MaterialIndex]; var shpkResource = ((Structs.MtrlResource*)material->MaterialResourceHandle)->ShpkResourceHandle; if ((nint)shpkResource != (nint)skinShpk.ResourceHandle) return _onRenderMaterialHook!.Original(human, param); @@ -164,6 +155,7 @@ public unsafe class SkinFixer : IDisposable // - Swapping path is taken up to hundreds of times a frame. // At the time of writing, the lock doesn't seem to have a noticeable impact in either framerate or CPU usage, but the swapping path shall still be avoided as much as possible. lock (_lock) + { try { _utility.Address->SkinShpkResource = (Structs.ResourceHandle*)skinShpk.ResourceHandle; @@ -173,5 +165,6 @@ public unsafe class SkinFixer : IDisposable { _utility.Address->SkinShpkResource = (Structs.ResourceHandle*)_utility.DefaultSkinShpkResource; } + } } } diff --git a/Penumbra/UI/Tabs/DebugTab.cs b/Penumbra/UI/Tabs/DebugTab.cs index 1ee62c35..c24d64fa 100644 --- a/Penumbra/UI/Tabs/DebugTab.cs +++ b/Penumbra/UI/Tabs/DebugTab.cs @@ -36,7 +36,6 @@ using ObjectKind = Dalamud.Game.ClientState.Objects.Enums.ObjectKind; using ResidentResourceManager = Penumbra.Interop.Services.ResidentResourceManager; using Penumbra.Interop.Services; using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel; -using static Lumina.Data.Parsing.Layer.LayerCommon; namespace Penumbra.UI.Tabs; @@ -623,18 +622,14 @@ public class DebugTab : Window, ITab ImGui.TableNextColumn(); if (resource == null) { - ImGui.TableNextColumn(); - ImGui.TableNextColumn(); - ImGui.TableNextColumn(); - ImGui.TableNextColumn(); + ImGui.TableNextRow(); continue; } UiHelpers.Text(resource); ImGui.TableNextColumn(); var data = (nint)ResourceHandle.GetData(resource); var length = ResourceHandle.GetLength(resource); - ImGui.Selectable($"0x{data:X}"); - if (ImGui.IsItemClicked()) + if (ImGui.Selectable($"0x{data:X}")) { if (data != nint.Zero && length > 0) ImGui.SetClipboardText(string.Join("\n", @@ -643,7 +638,7 @@ public class DebugTab : Window, ITab ImGuiUtil.HoverTooltip("Click to copy bytes to clipboard."); ImGui.TableNextColumn(); - ImGui.TextUnformatted($"{length}"); + ImGui.TextUnformatted(length.ToString()); ImGui.TableNextColumn(); if (intern.Value != -1)