diff --git a/OtterGui b/OtterGui index 2feb762d..9e98cb97 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 2feb762d72c8717d8ece6a0bf72298776312b2c1 +Subproject commit 9e98cb9722bed3129134c7bc2fbe51268b2d6acd diff --git a/Penumbra.GameData/Data/DisassembledShader.cs b/Penumbra.GameData/Data/DisassembledShader.cs index bfae7a74..f611982c 100644 --- a/Penumbra.GameData/Data/DisassembledShader.cs +++ b/Penumbra.GameData/Data/DisassembledShader.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Text; using System.Text.RegularExpressions; using Penumbra.GameData.Interop; +using Penumbra.String; namespace Penumbra.GameData.Data; @@ -96,26 +97,26 @@ public partial class DisassembledShader [GeneratedRegex(@"^\s*sample_\S*\s+[^.]+\.([wxyz]+),[^,]+,\s*t(\d+)\.([wxyz]+)", RegexOptions.NonBacktracking)] private static partial Regex Sm5TextureUsageRegex(); - private static readonly char[] Digits = Enumerable.Range(0, 10).Select(c => (char) ('0' + c)).ToArray(); + private static readonly char[] Digits = Enumerable.Range(0, 10).Select(c => (char)('0' + c)).ToArray(); - public readonly string RawDisassembly; - public readonly uint ShaderModel; - public readonly ShaderStage Stage; - public readonly string BufferDefinitions; - public readonly ResourceBinding[] ResourceBindings; - public readonly InputOutput[] InputSignature; - public readonly InputOutput[] OutputSignature; - public readonly string[] Instructions; + public readonly ByteString RawDisassembly; + public readonly uint ShaderModel; + public readonly ShaderStage Stage; + public readonly string BufferDefinitions; + public readonly ResourceBinding[] ResourceBindings; + public readonly InputOutput[] InputSignature; + public readonly InputOutput[] OutputSignature; + public readonly IReadOnlyList Instructions; - public DisassembledShader(string rawDisassembly) + public DisassembledShader(ByteString rawDisassembly) { RawDisassembly = rawDisassembly; - var lines = rawDisassembly.Split('\n'); - Instructions = Array.FindAll(lines, ln => !ln.StartsWith("//") && ln.Length > 0); - var shaderModel = Instructions[0].Trim().Split('_'); - Stage = (ShaderStage)(byte)char.ToUpper(shaderModel[0][0]); - ShaderModel = (uint.Parse(shaderModel[1]) << 8) | uint.Parse(shaderModel[2]); - var header = PreParseHeader(lines.AsSpan()[..Array.IndexOf(lines, Instructions[0])]); + var lines = rawDisassembly.Split((byte) '\n'); + Instructions = lines.FindAll(ln => !ln.StartsWith("//"u8) && ln.Length > 0); + var shaderModel = Instructions[0].Trim().Split((byte) '_'); + Stage = (ShaderStage)(byte)char.ToUpper((char) shaderModel[0][0]); + ShaderModel = (uint.Parse(shaderModel[1].ToString()) << 8) | uint.Parse(shaderModel[2].ToString()); + var header = PreParseHeader(lines.Take(lines.IndexOf(Instructions[0])).Select(l => l.ToString()).ToArray()); switch (ShaderModel >> 8) { case 3: @@ -142,8 +143,8 @@ public partial class DisassembledShader private static void ParseSm3Header(Dictionary header, out string bufferDefinitions, out ResourceBinding[] resourceBindings, out InputOutput[] inputSignature, out InputOutput[] outputSignature) { - bufferDefinitions = header.TryGetValue("Parameters", out var rawParameters) - ? string.Join('\n', rawParameters) + bufferDefinitions = header.TryGetValue("Parameters", out var rawParameters) + ? string.Join('\n', rawParameters) : string.Empty; if (header.TryGetValue("Registers", out var rawRegisters)) { @@ -176,7 +177,7 @@ public partial class DisassembledShader outputSignature = Array.Empty(); } - private static void ParseSm3ResourceUsage(string[] instructions, ResourceBinding[] resourceBindings) + private static void ParseSm3ResourceUsage(IReadOnlyList instructions, ResourceBinding[] resourceBindings) { var cbIndices = new Dictionary(); var tIndices = new Dictionary(); @@ -201,10 +202,11 @@ public partial class DisassembledShader foreach (var instruction in instructions) { var trimmed = instruction.Trim(); - if (trimmed.StartsWith("def") || trimmed.StartsWith("dcl")) + if (trimmed.StartsWith("def"u8) || trimmed.StartsWith("dcl"u8)) continue; - foreach (Match cbMatch in Sm3ConstantBufferUsageRegex().Matches(instruction)) + var instructionString = instruction.ToString(); + foreach (Match cbMatch in Sm3ConstantBufferUsageRegex().Matches(instructionString)) { var buffer = uint.Parse(cbMatch.Groups[1].Value); if (cbIndices.TryGetValue(buffer, out var i)) @@ -217,7 +219,7 @@ public partial class DisassembledShader } } - var tMatch = Sm3TextureUsageRegex().Match(instruction); + var tMatch = Sm3TextureUsageRegex().Match(instructionString); if (tMatch.Success) { var texture = uint.Parse(tMatch.Groups[1].Value); @@ -307,7 +309,7 @@ public partial class DisassembledShader } } - private static void ParseSm5ResourceUsage(string[] instructions, ResourceBinding[] resourceBindings) + private static void ParseSm5ResourceUsage(IReadOnlyList instructions, ResourceBinding[] resourceBindings) { var cbIndices = new Dictionary(); var tIndices = new Dictionary(); @@ -331,10 +333,11 @@ public partial class DisassembledShader foreach (var instruction in instructions) { var trimmed = instruction.Trim(); - if (trimmed.StartsWith("def") || trimmed.StartsWith("dcl")) + if (trimmed.StartsWith("def"u8) || trimmed.StartsWith("dcl"u8)) continue; - foreach (Match cbMatch in Sm5ConstantBufferUsageRegex().Matches(instruction)) + var instructionString = instruction.ToString(); + foreach (Match cbMatch in Sm5ConstantBufferUsageRegex().Matches(instructionString)) { var buffer = uint.Parse(cbMatch.Groups[1].Value); if (cbIndices.TryGetValue(buffer, out var i)) @@ -352,7 +355,7 @@ public partial class DisassembledShader } } - var tMatch = Sm5TextureUsageRegex().Match(instruction); + var tMatch = Sm5TextureUsageRegex().Match(instructionString); if (tMatch.Success) { var texture = uint.Parse(tMatch.Groups[2].Value); diff --git a/Penumbra.GameData/Interop/D3DCompiler.cs b/Penumbra.GameData/Interop/D3DCompiler.cs index ed6366c2..04bf1ba7 100644 --- a/Penumbra.GameData/Interop/D3DCompiler.cs +++ b/Penumbra.GameData/Interop/D3DCompiler.cs @@ -1,6 +1,7 @@ using System; using System.Runtime.InteropServices; using System.Text; +using Penumbra.String; namespace Penumbra.GameData.Interop; @@ -20,43 +21,41 @@ internal static class D3DCompiler [Flags] public enum DisassembleFlags : uint { - EnableColorCode = 1, - EnableDefaultValuePrints = 2, + EnableColorCode = 1, + EnableDefaultValuePrints = 2, EnableInstructionNumbering = 4, - EnableInstructionCycle = 8, - DisableDebugInfo = 16, - EnableInstructionOffset = 32, - InstructionOnly = 64, - PrintHexLiterals = 128, + EnableInstructionCycle = 8, + DisableDebugInfo = 16, + EnableInstructionOffset = 32, + InstructionOnly = 64, + PrintHexLiterals = 128, } - public static unsafe string Disassemble(ReadOnlySpan blob, DisassembleFlags flags = 0, string comments = "") + public static unsafe ByteString Disassemble(ReadOnlySpan blob, DisassembleFlags flags = 0, string comments = "") { - ID3DBlob? disassembly; - int hr; - fixed (byte* pSrcData = blob) + ID3DBlob? disassembly = null; + try { - hr = D3DDisassemble(pSrcData, new UIntPtr((uint)blob.Length), (uint)flags, comments, out disassembly); + fixed (byte* pSrcData = blob) + { + var hr = D3DDisassemble(pSrcData, new UIntPtr((uint)blob.Length), (uint)flags, comments, out disassembly); + Marshal.ThrowExceptionForHR(hr); + } + + return disassembly == null + ? ByteString.Empty + : new ByteString((byte*)disassembly.GetBufferPointer()).Clone(); } - Marshal.ThrowExceptionForHR(hr); - var ret = Encoding.UTF8.GetString(BlobContents(disassembly)); - GC.KeepAlive(disassembly); - return ret; - } - - private static unsafe ReadOnlySpan BlobContents(ID3DBlob? blob) - { - if (blob == null) + finally { - return ReadOnlySpan.Empty; + if (disassembly != null) + Marshal.FinalReleaseComObject(disassembly); } - - return new ReadOnlySpan(blob.GetBufferPointer(), (int)blob.GetBufferSize().ToUInt32()); } [PreserveSig] [DllImport("D3DCompiler_47.dll")] - private extern static unsafe int D3DDisassemble( + private static extern unsafe int D3DDisassemble( [In] byte* pSrcData, [In] UIntPtr srcDataSize, uint flags, diff --git a/Penumbra.String b/Penumbra.String index 3447fe0d..ce41e2b7 160000 --- a/Penumbra.String +++ b/Penumbra.String @@ -1 +1 @@ -Subproject commit 3447fe0dc9cfc5f056e1595c6c2413de37db924a +Subproject commit ce41e2b7da65edb25b5308ae41e4ca7d74d75e38 diff --git a/Penumbra/UI/Classes/ModEditWindow.Materials.Shpk.cs b/Penumbra/UI/Classes/ModEditWindow.Materials.Shpk.cs index 14719f88..5238d9a0 100644 --- a/Penumbra/UI/Classes/ModEditWindow.Materials.Shpk.cs +++ b/Penumbra/UI/Classes/ModEditWindow.Materials.Shpk.cs @@ -160,7 +160,7 @@ public partial class ModEditWindow { tab.NewKeyDefault = key.DefaultValue; tab.NewKeyId = key.Id; - ret = true; + ret = true; tab.UpdateShaderKeyLabels(); } } @@ -382,7 +382,9 @@ public partial class ModEditWindow var (label, filename) = tab.Samplers[ idx ]; using var tree = ImRaii.TreeNode( label ); if( !tree ) + { return false; + } ImRaii.TreeNode( filename, ImGuiTreeNodeFlags.Leaf ).Dispose(); var ret = false; @@ -398,7 +400,7 @@ public partial class ModEditWindow } ImGui.SetNextItemWidth( ImGuiHelpers.GlobalScale * 150.0f ); - if( InputHexUInt16( "Texture Flags", ref tab.Mtrl.Textures[sampler.TextureIndex].Flags, + if( InputHexUInt16( "Texture Flags", ref tab.Mtrl.Textures[ sampler.TextureIndex ].Flags, disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None ) ) { ret = true; @@ -409,22 +411,22 @@ public partial class ModEditWindow if( ImGui.InputInt( "Sampler Flags", ref samplerFlags, 0, 0, ImGuiInputTextFlags.CharsHexadecimal | ( disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None ) ) ) { - tab.Mtrl.ShaderPackage.Samplers[idx].Flags = ( uint )samplerFlags; - ret = true; + tab.Mtrl.ShaderPackage.Samplers[ idx ].Flags = ( uint )samplerFlags; + ret = true; } if( !disabled && tab.OrphanedSamplers.Count == 0 - && tab.AliasedSamplerCount == 0 + && tab.AliasedSamplerCount == 0 && ImGui.Button( "Remove Sampler" ) ) { - tab.Mtrl.Textures = tab.Mtrl.Textures.RemoveItems( sampler.TextureIndex ); + tab.Mtrl.Textures = tab.Mtrl.Textures.RemoveItems( sampler.TextureIndex ); tab.Mtrl.ShaderPackage.Samplers = tab.Mtrl.ShaderPackage.Samplers.RemoveItems( idx-- ); for( var i = 0; i < tab.Mtrl.ShaderPackage.Samplers.Length; ++i ) { - if( tab.Mtrl.ShaderPackage.Samplers[i].TextureIndex >= sampler.TextureIndex ) + if( tab.Mtrl.ShaderPackage.Samplers[ i ].TextureIndex >= sampler.TextureIndex ) { - --tab.Mtrl.ShaderPackage.Samplers[i].TextureIndex; + --tab.Mtrl.ShaderPackage.Samplers[ i ].TextureIndex; } } @@ -438,7 +440,7 @@ public partial class ModEditWindow private static bool DrawMaterialNewSampler( MtrlTab tab ) { - var (name, id) = tab.MissingSamplers[tab.NewSamplerIdx]; + var (name, id) = tab.MissingSamplers[ tab.NewSamplerIdx ]; ImGui.SetNextItemWidth( ImGuiHelpers.GlobalScale * 450.0f ); using( var c = ImRaii.Combo( "##NewSamplerId", $"{name} (ID: 0x{id:X8})" ) ) { @@ -475,14 +477,13 @@ public partial class ModEditWindow tab.UpdateSamplers(); tab.UpdateTextureLabels(); return true; - } private static bool DrawMaterialSamplers( MtrlTab tab, bool disabled ) { if( tab.Mtrl.ShaderPackage.Samplers.Length == 0 && tab.Mtrl.Textures.Length == 0 - && ( disabled || (tab.AssociatedShpk?.Samplers.All( sampler => sampler.Slot != 2 ) ?? false ) )) + && ( disabled || ( tab.AssociatedShpk?.Samplers.All( sampler => sampler.Slot != 2 ) ?? false ) ) ) { return false; } @@ -493,7 +494,7 @@ public partial class ModEditWindow return false; } - var ret = false; + var ret = false; for( var idx = 0; idx < tab.Mtrl.ShaderPackage.Samplers.Length; ++idx ) { ret |= DrawMaterialSampler( tab, disabled, ref idx ); @@ -511,7 +512,7 @@ public partial class ModEditWindow } } } - else if( !disabled && tab.MissingSamplers.Count > 0 && tab.AliasedSamplerCount == 0 && tab.Mtrl.Textures.Length < 255 ) + else if( !disabled && tab.MissingSamplers.Count > 0 && tab.AliasedSamplerCount == 0 && tab.Mtrl.Textures.Length < 255 ) { ret |= DrawMaterialNewSampler( tab ); } @@ -592,7 +593,7 @@ public partial class ModEditWindow } var sb = new StringBuilder( 128 ); - sb.Append( $"{prefix}[{firstVector}]{VectorSwizzle( firstComponent, 3 )}" ); + sb.Append( $"{prefix}[{firstVector}]{VectorSwizzle( firstComponent, 3 ).TrimEnd()}" ); for( var i = firstVector + 1; i < lastVector; ++i ) { sb.Append( $", [{i}]" ); diff --git a/Penumbra/UI/Classes/ModEditWindow.ShaderPackages.cs b/Penumbra/UI/Classes/ModEditWindow.ShaderPackages.cs index 9809874b..784c9e24 100644 --- a/Penumbra/UI/Classes/ModEditWindow.ShaderPackages.cs +++ b/Penumbra/UI/Classes/ModEditWindow.ShaderPackages.cs @@ -3,41 +3,38 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Numerics; -using Dalamud.Interface.ImGuiFileDialog; +using System.Text; using Dalamud.Interface.Internal.Notifications; using Dalamud.Interface; using ImGuiNET; using Lumina.Misc; using OtterGui.Raii; using OtterGui; -using OtterGui.Classes; using Penumbra.GameData; using Penumbra.GameData.Data; using Penumbra.GameData.Files; +using Penumbra.String; using Penumbra.Util; +using static Penumbra.GameData.Files.ShpkFile; namespace Penumbra.UI.Classes; public partial class ModEditWindow { - private readonly FileEditor< ShpkFile > _shaderPackageTab; + private static readonly ByteString DisassemblyLabel = ByteString.FromSpanUnsafe( "##disassembly"u8, true, true, true ); - private readonly FileDialogManager _shaderPackageFileDialog = ConfigWindow.SetupFileManager(); + private readonly FileEditor< ShpkTab > _shaderPackageTab; - private string _shaderPackageNewMaterialParamName = string.Empty; - private uint _shaderPackageNewMaterialParamId = Crc32.Get( string.Empty, 0xFFFFFFFFu ); - private ushort _shaderPackageNewMaterialParamStart = 0; - private ushort _shaderPackageNewMaterialParamEnd = 0; - - private bool DrawShaderPackagePanel( ShpkFile file, bool disabled ) + private static bool DrawShaderPackagePanel( ShpkTab file, bool disabled ) { - var ret = DrawShaderPackageSummary( file, disabled ); + DrawShaderPackageSummary( file ); + + var ret = false; + ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() / 2 ) ); + ret |= DrawShaderPackageShaderArray( file, "Vertex Shader", file.Shpk.VertexShaders, disabled ); ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() / 2 ) ); - ret |= DrawShaderPackageShaderArray( "Vertex Shader", file.VertexShaders, file, disabled ); - - ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() / 2 ) ); - ret |= DrawShaderPackageShaderArray( "Pixel Shader", file.PixelShaders, file, disabled ); + ret |= DrawShaderPackageShaderArray( file, "Pixel Shader", file.Shpk.PixelShaders, disabled ); ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() / 2 ) ); ret |= DrawShaderPackageMaterialParamLayout( file, disabled ); @@ -45,373 +42,183 @@ public partial class ModEditWindow ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() / 2 ) ); ret |= DrawOtherShaderPackageDetails( file, disabled ); - _shaderPackageFileDialog.Draw(); + file.FileDialog.Draw(); - ret |= file.IsChanged(); + ret |= file.Shpk.IsChanged(); return !disabled && ret; } - private static bool DrawShaderPackageSummary( ShpkFile file, bool _ ) - { - ImGui.Text( $"Shader Package for DirectX {( int )file.DirectXVersion}" ); + private static void DrawShaderPackageSummary( ShpkTab tab ) + => ImGui.TextUnformatted( tab.Header ); - return false; - } - - private bool DrawShaderPackageShaderArray( string objectName, ShpkFile.Shader[] shaders, ShpkFile file, bool disabled ) + private static void DrawShaderExportButton( ShpkTab tab, string objectName, Shader shader, int idx ) { - if( shaders.Length == 0 ) + if( !ImGui.Button( $"Export Shader Program Blob ({shader.Blob.Length} bytes)" ) ) { - return false; + return; } - if( !ImGui.CollapsingHeader( $"{objectName}s" ) ) + var defaultName = objectName[ 0 ] switch + { + 'V' => $"vs{idx}", + 'P' => $"ps{idx}", + _ => throw new NotImplementedException(), + }; + + var blob = shader.Blob; + tab.FileDialog.SaveFileDialog( $"Export {objectName} #{idx} Program Blob to...", tab.Extension, defaultName, tab.Extension, ( success, name ) => + { + if( !success ) + { + return; + } + + try + { + File.WriteAllBytes( name, blob ); + } + catch( Exception e ) + { + ChatUtil.NotificationMessage( $"Could not export {defaultName}{tab.Extension} to {name}:\n{e.Message}", "Penumbra Advanced Editing", + NotificationType.Error ); + return; + } + + ChatUtil.NotificationMessage( $"Shader Program Blob {defaultName}{tab.Extension} exported successfully to {Path.GetFileName( name )}", + "Penumbra Advanced Editing", NotificationType.Success ); + } ); + } + + private static void DrawShaderImportButton( ShpkTab tab, string objectName, Shader[] shaders, int idx ) + { + if( !ImGui.Button( "Replace Shader Program Blob" ) ) + { + return; + } + + tab.FileDialog.OpenFileDialog( $"Replace {objectName} #{idx} Program Blob...", "Shader Program Blobs{.o,.cso,.dxbc,.dxil}", ( success, name ) => + { + if( !success ) + { + return; + } + + try + { + shaders[ idx ].Blob = File.ReadAllBytes( name ); + } + catch( Exception e ) + { + ChatUtil.NotificationMessage( $"Could not import {name}:\n{e.Message}", "Penumbra Advanced Editing", NotificationType.Error ); + return; + } + + try + { + shaders[ idx ].UpdateResources( tab.Shpk ); + tab.Shpk.UpdateResources(); + } + catch( Exception e ) + { + tab.Shpk.SetInvalid(); + ChatUtil.NotificationMessage( $"Failed to update resources after importing {name}:\n{e.Message}", "Penumbra Advanced Editing", + NotificationType.Error ); + return; + } + + tab.Shpk.SetChanged(); + } ); + } + + private static unsafe void DrawRawDisassembly( Shader shader ) + { + using var t2 = ImRaii.TreeNode( "Raw Program Disassembly" ); + if( !t2 ) + { + return; + } + + using var font = ImRaii.PushFont( UiBuilder.MonoFont ); + var size = new Vector2( ImGui.GetContentRegionAvail().X, ImGui.GetTextLineHeight() * 20 ); + ImGuiNative.igInputTextMultiline( DisassemblyLabel.Path, shader.Disassembly!.RawDisassembly.Path, ( uint )shader.Disassembly!.RawDisassembly.Length + 1, size, + ImGuiInputTextFlags.ReadOnly, null, null ); + } + + private static bool DrawShaderPackageShaderArray( ShpkTab tab, string objectName, Shader[] shaders, bool disabled ) + { + if( shaders.Length == 0 || !ImGui.CollapsingHeader( $"{objectName}s" ) ) { return false; } var ret = false; - - foreach( var (shader, idx) in shaders.WithIndex() ) + for( var idx = 0; idx < shaders.Length; ++idx ) { - using var t = ImRaii.TreeNode( $"{objectName} #{idx}" ); - if( t ) + var shader = shaders[ idx ]; + using var t = ImRaii.TreeNode( $"{objectName} #{idx}" ); + if( !t ) { - if( ImGui.Button( $"Export Shader Program Blob ({shader.Blob.Length} bytes)" ) ) + continue; + } + + DrawShaderExportButton( tab, objectName, shader, idx ); + if( !disabled ) + { + ImGui.SameLine(); + DrawShaderImportButton( tab, objectName, shaders, idx ); + } + + ret |= DrawShaderPackageResourceArray( "Constant Buffers", "slot", true, shader.Constants, true ); + ret |= DrawShaderPackageResourceArray( "Samplers", "slot", false, shader.Samplers, true ); + ret |= DrawShaderPackageResourceArray( "Unordered Access Views", "slot", true, shader.Uavs, true ); + + if( shader.AdditionalHeader.Length > 0 ) + { + using var t2 = ImRaii.TreeNode( $"Additional Header (Size: {shader.AdditionalHeader.Length})###AdditionalHeader" ); + if( t2 ) { - var extension = file.DirectXVersion switch - { - ShpkFile.DxVersion.DirectX9 => ".cso", - ShpkFile.DxVersion.DirectX11 => ".dxbc", - _ => throw new NotImplementedException(), - }; - var defaultName = new string( objectName.Where( char.IsUpper ).ToArray() ).ToLower() + idx.ToString(); - var blob = shader.Blob; - _shaderPackageFileDialog.SaveFileDialog( $"Export {objectName} #{idx} Program Blob to...", extension, defaultName, extension, ( success, name ) => - { - if( !success ) - { - return; - } - - try - { - File.WriteAllBytes( name, blob ); - } - catch( Exception e ) - { - Penumbra.Log.Error( $"Could not export {defaultName}{extension} to {name}:\n{e}" ); - ChatUtil.NotificationMessage( $"Could not export {defaultName}{extension} to {Path.GetFileName( name )}:\n{e.Message}", "Penumbra Advanced Editing", - NotificationType.Error ); - return; - } - - ChatUtil.NotificationMessage( $"Shader Program Blob {defaultName}{extension} exported successfully to {Path.GetFileName( name )}", - "Penumbra Advanced Editing", NotificationType.Success ); - } ); - } - - if( !disabled ) - { - ImGui.SameLine(); - if( ImGui.Button( "Replace Shader Program Blob" ) ) - { - _shaderPackageFileDialog.OpenFileDialog( $"Replace {objectName} #{idx} Program Blob...", "Shader Program Blobs{.o,.cso,.dxbc,.dxil}", ( success, name ) => - { - if( !success ) - { - return; - } - - try - { - shaders[ idx ].Blob = File.ReadAllBytes( name ); - } - catch( Exception e ) - { - Penumbra.Log.Error( $"Could not import Shader Blob {name}:\n{e}" ); - ChatUtil.NotificationMessage( $"Could not import {Path.GetFileName( name )}:\n{e.Message}", "Penumbra Advanced Editing", NotificationType.Error ); - return; - } - - try - { - shaders[ idx ].UpdateResources( file ); - file.UpdateResources(); - } - catch( Exception e ) - { - file.SetInvalid(); - Penumbra.Log.Error( $"Failed to update resources after importing Shader Blob {name}:\n{e}" ); - ChatUtil.NotificationMessage( $"Failed to update resources after importing {Path.GetFileName( name )}:\n{e.Message}", "Penumbra Advanced Editing", - NotificationType.Error ); - return; - } - - file.SetChanged(); - ChatUtil.NotificationMessage( $"Shader Blob {Path.GetFileName( name )} imported successfully", "Penumbra Advanced Editing", NotificationType.Success ); - } ); - } - } - - ret |= DrawShaderPackageResourceArray( "Constant Buffers", "slot", true, shader.Constants, true ); - ret |= DrawShaderPackageResourceArray( "Samplers", "slot", false, shader.Samplers, true ); - ret |= DrawShaderPackageResourceArray( "Unordered Access Views", "slot", true, shader.Uavs, true ); - - if( shader.AdditionalHeader.Length > 0 ) - { - using var t2 = ImRaii.TreeNode( $"Additional Header (Size: {shader.AdditionalHeader.Length})###AdditionalHeader" ); - if( t2 ) - { - ImGuiUtil.TextWrapped( string.Join( ' ', shader.AdditionalHeader.Select( c => $"{c:X2}" ) ) ); - } - } - - using( var t2 = ImRaii.TreeNode( "Raw Program Disassembly" ) ) - { - if( t2 ) - { - using( var font = ImRaii.PushFont( UiBuilder.MonoFont ) ) - { - ImGui.TextUnformatted( shader.Disassembly!.RawDisassembly ); - } - } + ImGuiUtil.TextWrapped( string.Join( ' ', shader.AdditionalHeader.Select( c => $"{c:X2}" ) ) ); } } + + DrawRawDisassembly( shader ); } return ret; } - private bool DrawShaderPackageMaterialParamLayout( ShpkFile file, bool disabled ) + private static bool DrawShaderPackageResource( string slotLabel, bool withSize, ref Resource resource, bool disabled ) { var ret = false; - - var materialParams = file.GetConstantById( ShpkFile.MaterialParamsConstantId ); - - if( !ImGui.CollapsingHeader( $"{materialParams?.Name ?? "Material Parameter"} Layout" ) ) + if( !disabled ) { - return false; - } - - var isSizeWellDefined = ( file.MaterialParamsSize & 0xF ) == 0 && ( !materialParams.HasValue || file.MaterialParamsSize == materialParams.Value.Size << 4 ); - - if( !isSizeWellDefined ) - { - if( materialParams.HasValue ) + ImGui.SetNextItemWidth( ImGuiHelpers.GlobalScale * 150.0f ); + if( ImGuiUtil.InputUInt16( $"{char.ToUpper( slotLabel[ 0 ] )}{slotLabel[ 1.. ].ToLower()}", ref resource.Slot, ImGuiInputTextFlags.None ) ) { - ImGui.Text( $"Buffer size mismatch: {file.MaterialParamsSize} bytes ≠ {materialParams.Value.Size} registers ({materialParams.Value.Size << 4} bytes)" ); - } - else - { - ImGui.Text( $"Buffer size mismatch: {file.MaterialParamsSize} bytes, not a multiple of 16" ); + ret = true; } } - var parameters = new (uint, bool)?[( ( file.MaterialParamsSize + 0xFu ) & ~0xFu ) >> 2]; - var orphanParameters = new IndexSet( parameters.Length, true ); - var definedParameters = new HashSet< uint >(); - var hasMalformedParameters = false; - - foreach( var param in file.MaterialParams ) + if( resource.Used == null ) { - definedParameters.Add( param.Id ); - if( ( param.ByteOffset & 0x3 ) == 0 - && ( param.ByteSize & 0x3 ) == 0 - && param.ByteOffset + param.ByteSize <= file.MaterialParamsSize ) - { - var valueOffset = param.ByteOffset >> 2; - var valueCount = param.ByteSize >> 2; - orphanParameters.RemoveRange( valueOffset, valueCount ); - - parameters[ valueOffset ] = ( param.Id, true ); - - for( var i = 1; i < valueCount; ++i ) - { - parameters[ valueOffset + i ] = ( param.Id, false ); - } - } - else - { - hasMalformedParameters = true; - } + return ret; } - ImGui.Text( "Parameter positions (continuations are grayed out, unused values are red):" ); - - using( var table = ImRaii.Table( "##MaterialParamLayout", 5, - ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg ) ) + var usedString = UsedComponentString( withSize, resource ); + if( usedString.Length > 0 ) { - if( table ) - { - ImGui.TableNextColumn(); - ImGui.TableHeader( string.Empty ); - ImGui.TableNextColumn(); - ImGui.TableHeader( "x" ); - ImGui.TableNextColumn(); - ImGui.TableHeader( "y" ); - ImGui.TableNextColumn(); - ImGui.TableHeader( "z" ); - ImGui.TableNextColumn(); - ImGui.TableHeader( "w" ); - - var textColorStart = ImGui.GetColorU32( ImGuiCol.Text ); - var textColorCont = ( textColorStart & 0xFFFFFFu ) | ( ( textColorStart & 0xFE000000u ) >> 1 ); // Half opacity - var textColorUnusedStart = ( textColorStart & 0xFF000000u ) | ( ( textColorStart & 0xFEFEFE ) >> 1 ) | 0x80u; // Half red - var textColorUnusedCont = ( textColorUnusedStart & 0xFFFFFFu ) | ( ( textColorUnusedStart & 0xFE000000u ) >> 1 ); - - for( var idx = 0; idx < parameters.Length; idx += 4 ) - { - var usedComponents = ( materialParams?.Used?[ idx >> 2 ] ?? DisassembledShader.VectorComponents.All ) | ( materialParams?.UsedDynamically ?? 0 ); - ImGui.TableNextColumn(); - ImGui.Text( $"[{idx >> 2}]" ); - for( var col = 0; col < 4; ++col ) - { - var cell = parameters[ idx + col ]; - ImGui.TableNextColumn(); - var start = cell.HasValue && cell.Value.Item2; - var used = ( ( byte )usedComponents & ( 1 << col ) ) != 0; - using var c = ImRaii.PushColor( ImGuiCol.Text, used ? start ? textColorStart : textColorCont : start ? textColorUnusedStart : textColorUnusedCont ); - ImGui.Text( cell.HasValue ? $"0x{cell.Value.Item1:X8}" : "(none)" ); - } - - ImGui.TableNextRow(); - } - } + ImRaii.TreeNode( $"Used: {usedString}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet ).Dispose(); } - - if( hasMalformedParameters ) + else { - using var t = ImRaii.TreeNode( "Misaligned / Overflowing Parameters" ); - if( t ) - { - foreach( var param in file.MaterialParams ) - { - if( ( param.ByteOffset & 0x3 ) != 0 || ( param.ByteSize & 0x3 ) != 0 ) - { - ImRaii.TreeNode( $"ID: 0x{param.Id:X8}, offset: 0x{param.ByteOffset:X4}, size: 0x{param.ByteSize:X4}", ImGuiTreeNodeFlags.Leaf ).Dispose(); - } - else if( param.ByteOffset + param.ByteSize > file.MaterialParamsSize ) - { - ImRaii.TreeNode( $"{MaterialParamRangeName( materialParams?.Name ?? string.Empty, param.ByteOffset >> 2, param.ByteSize >> 2 )} (ID: 0x{param.Id:X8})", - ImGuiTreeNodeFlags.Leaf ).Dispose(); - } - } - } - } - else if( !disabled && isSizeWellDefined ) - { - using var t = ImRaii.TreeNode( "Add / Remove Parameters" ); - if( t ) - { - for( var i = 0; i < file.MaterialParams.Length; ++i ) - { - var param = file.MaterialParams[ i ]; - using var t2 = ImRaii.TreeNode( - $"{MaterialParamRangeName( materialParams?.Name ?? string.Empty, param.ByteOffset >> 2, param.ByteSize >> 2 ).Item1} (ID: 0x{param.Id:X8})" ); - if( t2 ) - { - if( ImGui.Button( "Remove" ) ) - { - file.MaterialParams = file.MaterialParams.RemoveItems( i ); - ret = true; - } - } - } - - if( orphanParameters.Count > 0 ) - { - using var t2 = ImRaii.TreeNode( "New Parameter" ); - if( t2 ) - { - var starts = orphanParameters.ToArray(); - if( !orphanParameters[ _shaderPackageNewMaterialParamStart ] ) - { - _shaderPackageNewMaterialParamStart = ( ushort )starts[ 0 ]; - } - - ImGui.SetNextItemWidth( ImGuiHelpers.GlobalScale * 225.0f ); - var startName = MaterialParamName( false, _shaderPackageNewMaterialParamStart )!; - using( var c = ImRaii.Combo( "Start", $"{materialParams?.Name ?? ""}{startName}" ) ) - { - if( c ) - { - foreach( var start in starts ) - { - var name = MaterialParamName( false, start )!; - if( ImGui.Selectable( $"{materialParams?.Name ?? ""}{name}", start == _shaderPackageNewMaterialParamStart ) ) - { - _shaderPackageNewMaterialParamStart = ( ushort )start; - } - } - } - } - - var lastEndCandidate = ( int )_shaderPackageNewMaterialParamStart; - var ends = starts.SkipWhile( i => i < _shaderPackageNewMaterialParamStart ).TakeWhile( i => - { - var ret = i <= lastEndCandidate + 1; - lastEndCandidate = i; - return ret; - } ).ToArray(); - if( Array.IndexOf( ends, _shaderPackageNewMaterialParamEnd ) < 0 ) - { - _shaderPackageNewMaterialParamEnd = ( ushort )ends[ 0 ]; - } - - ImGui.SetNextItemWidth( ImGuiHelpers.GlobalScale * 225.0f ); - var endName = MaterialParamName( false, _shaderPackageNewMaterialParamEnd )!; - using( var c = ImRaii.Combo( "End", $"{materialParams?.Name ?? ""}{endName}" ) ) - { - if( c ) - { - foreach( var end in ends ) - { - var name = MaterialParamName( false, end )!; - if( ImGui.Selectable( $"{materialParams?.Name ?? ""}{name}", end == _shaderPackageNewMaterialParamEnd ) ) - { - _shaderPackageNewMaterialParamEnd = ( ushort )end; - } - } - } - } - - ImGui.SetNextItemWidth( ImGuiHelpers.GlobalScale * 225.0f ); - if( ImGui.InputText( $"Name", ref _shaderPackageNewMaterialParamName, 63 ) ) - { - _shaderPackageNewMaterialParamId = Crc32.Get( _shaderPackageNewMaterialParamName, 0xFFFFFFFFu ); - } - - ImGui.SameLine(); - ImGui.Text( $"(ID: 0x{_shaderPackageNewMaterialParamId:X8})" ); - if( ImGui.Button( "Add" ) ) - { - if( definedParameters.Contains( _shaderPackageNewMaterialParamId ) ) - { - ChatUtil.NotificationMessage( $"Duplicate parameter ID 0x{_shaderPackageNewMaterialParamId:X8}", "Penumbra Advanced Editing", - NotificationType.Error ); - } - else - { - file.MaterialParams = file.MaterialParams.AddItem( new ShpkFile.MaterialParam - { - Id = _shaderPackageNewMaterialParamId, - ByteOffset = ( ushort )( _shaderPackageNewMaterialParamStart << 2 ), - ByteSize = ( ushort )( ( _shaderPackageNewMaterialParamEnd + 1 - _shaderPackageNewMaterialParamStart ) << 2 ), - } ); - ret = true; - } - } - } - } - } + ImRaii.TreeNode( "Unused", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet ).Dispose(); } return ret; } - private static bool DrawShaderPackageResourceArray( string arrayName, string slotLabel, bool withSize, ShpkFile.Resource[] resources, bool disabled ) + private static bool DrawShaderPackageResourceArray( string arrayName, string slotLabel, bool withSize, Resource[] resources, bool disabled ) { if( resources.Length == 0 ) { @@ -425,95 +232,109 @@ public partial class ModEditWindow } var ret = false; - - foreach( var (buf, idx) in resources.WithIndex() ) + for( var idx = 0; idx < resources.Length; ++idx ) { - using var t2 = ImRaii.TreeNode( - $"#{idx}: {buf.Name} (ID: 0x{buf.Id:X8}), {slotLabel}: {buf.Slot}" - + ( withSize ? $", size: {buf.Size} registers###{idx}: {buf.Name} (ID: 0x{buf.Id:X8})" : string.Empty ), - !disabled || buf.Used != null ? 0 : ImGuiTreeNodeFlags.Leaf ); + ref var buf = ref resources[ idx ]; + var name = $"#{idx}: {buf.Name} (ID: 0x{buf.Id:X8}), {slotLabel}: {buf.Slot}" + + ( withSize ? $", size: {buf.Size} registers###{idx}: {buf.Name} (ID: 0x{buf.Id:X8})" : string.Empty ); + using var font = ImRaii.PushFont( UiBuilder.MonoFont ); + using var t2 = ImRaii.TreeNode( name, !disabled || buf.Used != null ? 0 : ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet ); + font.Dispose(); if( t2 ) { - if( !disabled ) + ret |= DrawShaderPackageResource( slotLabel, withSize, ref buf, disabled ); + } + } + + return ret; + } + + private static bool DrawMaterialParamLayoutHeader( string label ) + { + using var font = ImRaii.PushFont( UiBuilder.MonoFont ); + var pos = ImGui.GetCursorScreenPos() + + new Vector2( ImGui.CalcTextSize( label ).X + 3 * ImGui.GetStyle().ItemInnerSpacing.X + ImGui.GetFrameHeight(), ImGui.GetStyle().FramePadding.Y ); + + var ret = ImGui.CollapsingHeader( label ); + ImGui.GetWindowDrawList().AddText( UiBuilder.DefaultFont, UiBuilder.DefaultFont.FontSize, pos, ImGui.GetColorU32( ImGuiCol.Text ), "Layout" ); + return ret; + } + + private static bool DrawMaterialParamLayoutBufferSize( ShpkFile file, Resource? materialParams ) + { + var isSizeWellDefined = ( file.MaterialParamsSize & 0xF ) == 0 && ( !materialParams.HasValue || file.MaterialParamsSize == materialParams.Value.Size << 4 ); + if( isSizeWellDefined ) + { + return true; + } + + ImGui.TextUnformatted( materialParams.HasValue + ? $"Buffer size mismatch: {file.MaterialParamsSize} bytes ≠ {materialParams.Value.Size} registers ({materialParams.Value.Size << 4} bytes)" + : $"Buffer size mismatch: {file.MaterialParamsSize} bytes, not a multiple of 16" ); + return false; + } + + private static bool DrawShaderPackageMaterialMatrix( ShpkTab tab, bool disabled ) + { + ImGui.TextUnformatted( "Parameter positions (continuations are grayed out, unused values are red):" ); + + using var table = ImRaii.Table( "##MaterialParamLayout", 5, + ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg ); + if( !table ) + { + return false; + } + + ImGui.TableSetupColumn( string.Empty, ImGuiTableColumnFlags.WidthFixed, 25 * ImGuiHelpers.GlobalScale ); + ImGui.TableSetupColumn( "x", ImGuiTableColumnFlags.WidthFixed, 100 * ImGuiHelpers.GlobalScale ); + ImGui.TableSetupColumn( "y", ImGuiTableColumnFlags.WidthFixed, 100 * ImGuiHelpers.GlobalScale ); + ImGui.TableSetupColumn( "z", ImGuiTableColumnFlags.WidthFixed, 100 * ImGuiHelpers.GlobalScale ); + ImGui.TableSetupColumn( "w", ImGuiTableColumnFlags.WidthFixed, 100 * ImGuiHelpers.GlobalScale ); + ImGui.TableHeadersRow(); + + var textColorStart = ImGui.GetColorU32( ImGuiCol.Text ); + var textColorCont = ( textColorStart & 0x00FFFFFFu ) | ( ( textColorStart & 0xFE000000u ) >> 1 ); // Half opacity + var textColorUnusedStart = ( textColorStart & 0xFF000000u ) | ( ( textColorStart & 0x00FEFEFE ) >> 1 ) | 0x80u; // Half red + var textColorUnusedCont = ( textColorUnusedStart & 0x00FFFFFFu ) | ( ( textColorUnusedStart & 0xFE000000u ) >> 1 ); + + var ret = false; + for( var i = 0; i < tab.Matrix.GetLength( 0 ); ++i ) + { + ImGui.TableNextColumn(); + ImGui.TableHeader( $" [{i}]" ); + for( var j = 0; j < 4; ++j ) + { + var (name, tooltip, idx, colorType) = tab.Matrix[ i, j ]; + var color = colorType switch { - // FIXME this probably doesn't belong here - static unsafe bool InputUInt16( string label, ref ushort v, ImGuiInputTextFlags flags ) + ShpkTab.ColorType.Unused => textColorUnusedStart, + ShpkTab.ColorType.Used => textColorStart, + ShpkTab.ColorType.Continuation => textColorUnusedCont, + ShpkTab.ColorType.Continuation | ShpkTab.ColorType.Used => textColorCont, + _ => textColorStart, + }; + using var _ = ImRaii.PushId( i * 4 + j ); + var deletable = !disabled && idx >= 0; + using( var font = ImRaii.PushFont( UiBuilder.MonoFont, tooltip.Length > 0 ) ) + { + using( var c = ImRaii.PushColor( ImGuiCol.Text, color ) ) { - fixed( ushort* v2 = &v ) + ImGui.TableNextColumn(); + ImGui.Selectable( name ); + if( deletable && ImGui.IsItemClicked( ImGuiMouseButton.Right ) && ImGui.GetIO().KeyCtrl ) { - return ImGui.InputScalar( label, ImGuiDataType.U16, new nint( v2 ), nint.Zero, nint.Zero, "%hu", flags ); + tab.Shpk.MaterialParams = tab.Shpk.MaterialParams.RemoveItems( idx ); + ret = true; + tab.Update(); } } - ImGui.SetNextItemWidth( ImGuiHelpers.GlobalScale * 150.0f ); - if( InputUInt16( $"{char.ToUpper( slotLabel[ 0 ] )}{slotLabel[ 1.. ].ToLower()}", ref resources[ idx ].Slot, ImGuiInputTextFlags.None ) ) - { - ret = true; - } + ImGuiUtil.HoverTooltip( tooltip ); } - if( buf.Used != null ) + if( deletable ) { - var used = new List< string >(); - if( withSize ) - { - foreach( var (components, i) in ( buf.Used ?? Array.Empty< DisassembledShader.VectorComponents >() ).WithIndex() ) - { - switch( components ) - { - case 0: break; - case DisassembledShader.VectorComponents.All: - used.Add( $"[{i}]" ); - break; - default: - used.Add( $"[{i}].{new string( components.ToString().Where( char.IsUpper ).ToArray() ).ToLower()}" ); - break; - } - } - - switch( buf.UsedDynamically ?? 0 ) - { - case 0: break; - case DisassembledShader.VectorComponents.All: - used.Add( "[*]" ); - break; - default: - used.Add( $"[*].{new string( buf.UsedDynamically!.Value.ToString().Where( char.IsUpper ).ToArray() ).ToLower()}" ); - break; - } - } - else - { - var components = ( buf.Used != null && buf.Used.Length > 0 ? buf.Used[ 0 ] : 0 ) | ( buf.UsedDynamically ?? 0 ); - if( ( components & DisassembledShader.VectorComponents.X ) != 0 ) - { - used.Add( "Red" ); - } - - if( ( components & DisassembledShader.VectorComponents.Y ) != 0 ) - { - used.Add( "Green" ); - } - - if( ( components & DisassembledShader.VectorComponents.Z ) != 0 ) - { - used.Add( "Blue" ); - } - - if( ( components & DisassembledShader.VectorComponents.W ) != 0 ) - { - used.Add( "Alpha" ); - } - } - - if( used.Count > 0 ) - { - ImRaii.TreeNode( $"Used: {string.Join( ", ", used )}", ImGuiTreeNodeFlags.Leaf ).Dispose(); - } - else - { - ImRaii.TreeNode( "Unused", ImGuiTreeNodeFlags.Leaf ).Dispose(); - } + ImGuiUtil.HoverTooltip( "\nControl + Right-Click to remove." ); } } } @@ -521,7 +342,214 @@ public partial class ModEditWindow return ret; } - private static bool DrawOtherShaderPackageDetails( ShpkFile file, bool disabled ) + private static void DrawShaderPackageMisalignedParameters( ShpkTab tab ) + { + using var t = ImRaii.TreeNode( "Misaligned / Overflowing Parameters" ); + if( !t ) + { + return; + } + + using var _ = ImRaii.PushFont( UiBuilder.MonoFont ); + foreach( var name in tab.MalformedParameters ) + { + ImRaii.TreeNode( name, ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet ).Dispose(); + } + } + + private static void DrawShaderPackageStartCombo( ShpkTab tab ) + { + using var s = ImRaii.PushStyle( ImGuiStyleVar.ItemSpacing, ImGui.GetStyle().ItemInnerSpacing ); + using( var _ = ImRaii.PushFont( UiBuilder.MonoFont ) ) + { + ImGui.SetNextItemWidth( ImGuiHelpers.GlobalScale * 400 ); + using var c = ImRaii.Combo( "##Start", tab.Orphans[ tab.NewMaterialParamStart ].Name ); + if( c ) + { + foreach( var (start, idx) in tab.Orphans.WithIndex() ) + { + if( ImGui.Selectable( start.Name, idx == tab.NewMaterialParamStart ) ) + { + tab.UpdateOrphanStart( idx ); + } + } + } + } + + ImGui.SameLine(); + ImGui.TextUnformatted( "Start" ); + } + + private static void DrawShaderPackageEndCombo( ShpkTab tab ) + { + using var s = ImRaii.PushStyle( ImGuiStyleVar.ItemSpacing, ImGui.GetStyle().ItemInnerSpacing ); + using( var _ = ImRaii.PushFont( UiBuilder.MonoFont ) ) + { + ImGui.SetNextItemWidth( ImGuiHelpers.GlobalScale * 400 ); + using var c = ImRaii.Combo( "##End", tab.Orphans[ tab.NewMaterialParamEnd ].Name ); + if( c ) + { + var current = tab.Orphans[ tab.NewMaterialParamStart ].Index; + for( var i = tab.NewMaterialParamStart; i < tab.Orphans.Count; ++i ) + { + var next = tab.Orphans[ i ]; + if( current++ != next.Index ) + { + break; + } + + if( ImGui.Selectable( next.Name, i == tab.NewMaterialParamEnd ) ) + { + tab.NewMaterialParamEnd = i; + } + } + } + } + + ImGui.SameLine(); + ImGui.TextUnformatted( "End" ); + } + + private static bool DrawShaderPackageNewParameter( ShpkTab tab ) + { + if( tab.Orphans.Count == 0 ) + { + return false; + } + + DrawShaderPackageStartCombo( tab ); + DrawShaderPackageEndCombo( tab ); + + ImGui.SetNextItemWidth( ImGuiHelpers.GlobalScale * 400 ); + if( ImGui.InputText( "Name", ref tab.NewMaterialParamName, 63 ) ) + { + tab.NewMaterialParamId = Crc32.Get( tab.NewMaterialParamName, 0xFFFFFFFFu ); + } + + var tooltip = tab.UsedIds.Contains( tab.NewMaterialParamId ) + ? "The ID is already in use. Please choose a different name." + : string.Empty; + if( !ImGuiUtil.DrawDisabledButton( $"Add ID 0x{tab.NewMaterialParamId:X8}", new Vector2( 400 * ImGuiHelpers.GlobalScale, ImGui.GetFrameHeight() ), tooltip, + tooltip.Length > 0 ) ) + { + return false; + } + + tab.Shpk.MaterialParams = tab.Shpk.MaterialParams.AddItem( new MaterialParam + { + Id = tab.NewMaterialParamId, + ByteOffset = ( ushort )( tab.Orphans[ tab.NewMaterialParamStart ].Index << 2 ), + ByteSize = ( ushort )( ( tab.NewMaterialParamEnd - tab.NewMaterialParamStart + 1 ) << 2 ), + } ); + tab.Update(); + return true; + } + + private static bool DrawShaderPackageMaterialParamLayout( ShpkTab tab, bool disabled ) + { + var ret = false; + + var materialParams = tab.Shpk.GetConstantById( MaterialParamsConstantId ); + if( !DrawMaterialParamLayoutHeader( materialParams?.Name ?? "Material Parameter" ) ) + { + return false; + } + + var sizeWellDefined = DrawMaterialParamLayoutBufferSize( tab.Shpk, materialParams ); + + ret |= DrawShaderPackageMaterialMatrix( tab, disabled ); + + if( tab.MalformedParameters.Count > 0 ) + { + DrawShaderPackageMisalignedParameters( tab ); + } + else if( !disabled && sizeWellDefined ) + { + ret |= DrawShaderPackageNewParameter( tab ); + } + + return ret; + } + + private static void DrawKeyArray( string arrayName, bool withId, IReadOnlyCollection< Key > keys ) + { + if( keys.Count == 0 ) + { + return; + } + + using var t = ImRaii.TreeNode( arrayName ); + if( !t ) + { + return; + } + + using var font = ImRaii.PushFont( UiBuilder.MonoFont ); + foreach( var (key, idx) in keys.WithIndex() ) + { + using var t2 = ImRaii.TreeNode( withId ? $"#{idx}: ID: 0x{key.Id:X8}" : $"#{idx}" ); + if( t2 ) + { + ImRaii.TreeNode( $"Default Value: 0x{key.DefaultValue:X8}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet ).Dispose(); + ImRaii.TreeNode( $"Known Values: {string.Join( ", ", Array.ConvertAll( key.Values, value => $"0x{value:X8}" ) )}", + ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet ).Dispose(); + } + } + } + + private static void DrawShaderPackageNodes( ShpkTab tab ) + { + if( tab.Shpk.Nodes.Length <= 0 ) + { + return; + } + + using var t = ImRaii.TreeNode( $"Nodes ({tab.Shpk.Nodes.Length})###Nodes" ); + if( !t ) + { + return; + } + + foreach( var (node, idx) in tab.Shpk.Nodes.WithIndex() ) + { + using var font = ImRaii.PushFont( UiBuilder.MonoFont ); + using var t2 = ImRaii.TreeNode( $"#{idx:D4}: ID: 0x{node.Id:X8}" ); + if( !t2 ) + { + continue; + } + + foreach( var (key, keyIdx) in node.SystemKeys.WithIndex() ) + { + ImRaii.TreeNode( $"System Key 0x{tab.Shpk.SystemKeys[ keyIdx ].Id:X8} = 0x{key:X8}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet ).Dispose(); + } + + foreach( var (key, keyIdx) in node.SceneKeys.WithIndex() ) + { + ImRaii.TreeNode( $"Scene Key 0x{tab.Shpk.SceneKeys[ keyIdx ].Id:X8} = 0x{key:X8}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet ).Dispose(); + } + + foreach( var (key, keyIdx) in node.MaterialKeys.WithIndex() ) + { + ImRaii.TreeNode( $"Material Key 0x{tab.Shpk.MaterialKeys[ keyIdx ].Id:X8} = 0x{key:X8}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet ).Dispose(); + } + + foreach( var (key, keyIdx) in node.SubViewKeys.WithIndex() ) + { + ImRaii.TreeNode( $"Sub-View Key #{keyIdx} = 0x{key:X8}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet ).Dispose(); + } + + ImRaii.TreeNode( $"Pass Indices: {string.Join( ' ', node.PassIndices.Select( c => $"{c:X2}" ) )}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet ).Dispose(); + foreach( var (pass, passIdx) in node.Passes.WithIndex() ) + { + ImRaii.TreeNode( $"Pass #{passIdx}: ID: 0x{pass.Id:X8}, Vertex Shader #{pass.VertexShader}, Pixel Shader #{pass.PixelShader}", + ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet ) + .Dispose(); + } + } + } + + private static bool DrawOtherShaderPackageDetails( ShpkTab tab, bool disabled ) { var ret = false; @@ -530,105 +558,109 @@ public partial class ModEditWindow return false; } - ImRaii.TreeNode( $"Version: 0x{file.Version:X8}", ImGuiTreeNodeFlags.Leaf ).Dispose(); + ImRaii.TreeNode( $"Version: 0x{tab.Shpk.Version:X8}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet ).Dispose(); - ret |= DrawShaderPackageResourceArray( "Constant Buffers", "type", true, file.Constants, disabled ); - ret |= DrawShaderPackageResourceArray( "Samplers", "type", false, file.Samplers, disabled ); - ret |= DrawShaderPackageResourceArray( "Unordered Access Views", "type", false, file.Uavs, disabled ); + ret |= DrawShaderPackageResourceArray( "Constant Buffers", "type", true, tab.Shpk.Constants, disabled ); + ret |= DrawShaderPackageResourceArray( "Samplers", "type", false, tab.Shpk.Samplers, disabled ); + ret |= DrawShaderPackageResourceArray( "Unordered Access Views", "type", false, tab.Shpk.Uavs, disabled ); - static bool DrawKeyArray( string arrayName, bool withId, ShpkFile.Key[] keys, bool _ ) + DrawKeyArray( "System Keys", true, tab.Shpk.SystemKeys ); + DrawKeyArray( "Scene Keys", true, tab.Shpk.SceneKeys ); + DrawKeyArray( "Material Keys", true, tab.Shpk.MaterialKeys ); + DrawKeyArray( "Sub-View Keys", false, tab.Shpk.SubViewKeys ); + + DrawShaderPackageNodes( tab ); + if( tab.Shpk.Items.Length > 0 ) { - if( keys.Length == 0 ) - { - return false; - } - - using var t = ImRaii.TreeNode( arrayName ); - if( !t ) - { - return false; - } - - foreach( var (key, idx) in keys.WithIndex() ) - { - using var t2 = ImRaii.TreeNode( withId ? $"#{idx}: ID: 0x{key.Id:X8}" : $"#{idx}" ); - if( t2 ) - { - ImRaii.TreeNode( $"Default Value: 0x{key.DefaultValue:X8}", ImGuiTreeNodeFlags.Leaf ).Dispose(); - ImRaii.TreeNode( $"Known Values: {string.Join( ", ", Array.ConvertAll( key.Values, value => $"0x{value:X8}" ) )}", ImGuiTreeNodeFlags.Leaf ).Dispose(); - } - } - - return false; - } - - ret |= DrawKeyArray( "System Keys", true, file.SystemKeys, disabled ); - ret |= DrawKeyArray( "Scene Keys", true, file.SceneKeys, disabled ); - ret |= DrawKeyArray( "Material Keys", true, file.MaterialKeys, disabled ); - ret |= DrawKeyArray( "Sub-View Keys", false, file.SubViewKeys, disabled ); - - if( file.Nodes.Length > 0 ) - { - using var t = ImRaii.TreeNode( $"Nodes ({file.Nodes.Length})" ); + using var t = ImRaii.TreeNode( $"Items ({tab.Shpk.Items.Length})###Items" ); if( t ) { - foreach( var (node, idx) in file.Nodes.WithIndex() ) + using var font = ImRaii.PushFont( UiBuilder.MonoFont ); + foreach( var (item, idx) in tab.Shpk.Items.WithIndex() ) { - using var t2 = ImRaii.TreeNode( $"#{idx}: ID: 0x{node.Id:X8}" ); - if( t2 ) - { - foreach( var (key, keyIdx) in node.SystemKeys.WithIndex() ) - { - ImRaii.TreeNode( $"System Key 0x{file.SystemKeys[ keyIdx ].Id:X8} = 0x{key:X8}", ImGuiTreeNodeFlags.Leaf ).Dispose(); - } - - foreach( var (key, keyIdx) in node.SceneKeys.WithIndex() ) - { - ImRaii.TreeNode( $"Scene Key 0x{file.SceneKeys[ keyIdx ].Id:X8} = 0x{key:X8}", ImGuiTreeNodeFlags.Leaf ).Dispose(); - } - - foreach( var (key, keyIdx) in node.MaterialKeys.WithIndex() ) - { - ImRaii.TreeNode( $"Material Key 0x{file.MaterialKeys[ keyIdx ].Id:X8} = 0x{key:X8}", ImGuiTreeNodeFlags.Leaf ).Dispose(); - } - - foreach( var (key, keyIdx) in node.SubViewKeys.WithIndex() ) - { - ImRaii.TreeNode( $"Sub-View Key #{keyIdx} = 0x{key:X8}", ImGuiTreeNodeFlags.Leaf ).Dispose(); - } - - ImRaii.TreeNode( $"Pass Indices: {string.Join( ' ', node.PassIndices.Select( c => $"{c:X2}" ) )}", ImGuiTreeNodeFlags.Leaf ).Dispose(); - foreach( var (pass, passIdx) in node.Passes.WithIndex() ) - { - ImRaii.TreeNode( $"Pass #{passIdx}: ID: 0x{pass.Id:X8}, Vertex Shader #{pass.VertexShader}, Pixel Shader #{pass.PixelShader}", ImGuiTreeNodeFlags.Leaf ) - .Dispose(); - } - } + ImRaii.TreeNode( $"#{idx:D4}: ID: 0x{item.Id:X8}, node: {item.Node}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet ).Dispose(); } } } - if( file.Items.Length > 0 ) + if( tab.Shpk.AdditionalData.Length > 0 ) { - using var t = ImRaii.TreeNode( $"Items ({file.Items.Length})" ); + using var t = ImRaii.TreeNode( $"Additional Data (Size: {tab.Shpk.AdditionalData.Length})###AdditionalData" ); if( t ) { - foreach( var (item, idx) in file.Items.WithIndex() ) - { - ImRaii.TreeNode( $"#{idx}: ID: 0x{item.Id:X8}, node: {item.Node}", ImGuiTreeNodeFlags.Leaf ).Dispose(); - } - } - } - - if( file.AdditionalData.Length > 0 ) - { - using var t = ImRaii.TreeNode( $"Additional Data (Size: {file.AdditionalData.Length})###AdditionalData" ); - if( t ) - { - ImGuiUtil.TextWrapped( string.Join( ' ', file.AdditionalData.Select( c => $"{c:X2}" ) ) ); + ImGuiUtil.TextWrapped( string.Join( ' ', tab.Shpk.AdditionalData.Select( c => $"{c:X2}" ) ) ); } } return ret; } + + private static string UsedComponentString( bool withSize, in Resource resource ) + { + var sb = new StringBuilder( 256 ); + if( withSize ) + { + foreach( var (components, i) in ( resource.Used ?? Array.Empty< DisassembledShader.VectorComponents >() ).WithIndex() ) + { + switch( components ) + { + case 0: break; + case DisassembledShader.VectorComponents.All: + sb.Append( $"[{i}], " ); + break; + default: + sb.Append( $"[{i}]." ); + foreach( var c in components.ToString().Where( char.IsUpper ) ) + { + sb.Append( char.ToLower( c ) ); + } + + sb.Append( ", " ); + break; + } + } + + switch( resource.UsedDynamically ?? 0 ) + { + case 0: break; + case DisassembledShader.VectorComponents.All: + sb.Append( "[*], " ); + break; + default: + sb.Append( "[*]." ); + foreach( var c in resource.UsedDynamically!.Value.ToString().Where( char.IsUpper ) ) + { + sb.Append( char.ToLower( c ) ); + } + + sb.Append( ", " ); + break; + } + } + else + { + var components = ( resource.Used is { Length: > 0 } ? resource.Used[ 0 ] : 0 ) | ( resource.UsedDynamically ?? 0 ); + if( ( components & DisassembledShader.VectorComponents.X ) != 0 ) + { + sb.Append( "Red, " ); + } + + if( ( components & DisassembledShader.VectorComponents.Y ) != 0 ) + { + sb.Append( "Green, " ); + } + + if( ( components & DisassembledShader.VectorComponents.Z ) != 0 ) + { + sb.Append( "Blue, " ); + } + + if( ( components & DisassembledShader.VectorComponents.W ) != 0 ) + { + sb.Append( "Alpha, " ); + } + } + + return sb.Length == 0 ? string.Empty : sb.ToString( 0, sb.Length - 2 ); + } } \ No newline at end of file diff --git a/Penumbra/UI/Classes/ModEditWindow.ShpkTab.cs b/Penumbra/UI/Classes/ModEditWindow.ShpkTab.cs new file mode 100644 index 00000000..448d8e35 --- /dev/null +++ b/Penumbra/UI/Classes/ModEditWindow.ShpkTab.cs @@ -0,0 +1,188 @@ +using System; +using System.Collections.Generic; +using Dalamud.Interface.ImGuiFileDialog; +using Dalamud.Utility; +using Lumina.Misc; +using OtterGui; +using Penumbra.GameData.Data; +using Penumbra.GameData.Files; + +namespace Penumbra.UI.Classes; + +public partial class ModEditWindow +{ + private class ShpkTab : IWritable + { + public readonly ShpkFile Shpk; + + public string NewMaterialParamName = string.Empty; + public uint NewMaterialParamId = Crc32.Get( string.Empty, 0xFFFFFFFFu ); + public short NewMaterialParamStart; + public short NewMaterialParamEnd; + + public readonly FileDialogManager FileDialog = ConfigWindow.SetupFileManager(); + + public readonly string Header; + public readonly string Extension; + + public ShpkTab( byte[] bytes ) + { + Shpk = new ShpkFile( bytes, true ); + Header = $"Shader Package for DirectX {( int )Shpk.DirectXVersion}"; + Extension = Shpk.DirectXVersion switch + { + ShpkFile.DxVersion.DirectX9 => ".cso", + ShpkFile.DxVersion.DirectX11 => ".dxbc", + _ => throw new NotImplementedException(), + }; + Update(); + } + + [Flags] + public enum ColorType : byte + { + Unused = 0, + Used = 1, + Continuation = 2, + } + + public (string Name, string Tooltip, short Index, ColorType Color)[,] Matrix = null!; + public readonly List< string > MalformedParameters = new(); + public readonly HashSet< uint > UsedIds = new(16); + public readonly List< (string Name, short Index) > Orphans = new(16); + + public void Update() + { + var materialParams = Shpk.GetConstantById( ShpkFile.MaterialParamsConstantId ); + var numParameters = ( ( Shpk.MaterialParamsSize + 0xFu ) & ~0xFu ) >> 4; + Matrix = new (string Name, string Tooltip, short Index, ColorType Color)[numParameters, 4]; + + MalformedParameters.Clear(); + UsedIds.Clear(); + foreach( var (param, idx) in Shpk.MaterialParams.WithIndex() ) + { + UsedIds.Add( param.Id ); + var iStart = param.ByteOffset >> 4; + var jStart = ( param.ByteOffset >> 2 ) & 3; + var iEnd = ( param.ByteOffset + param.ByteSize - 1 ) >> 4; + var jEnd = ( ( param.ByteOffset + param.ByteSize - 1 ) >> 2 ) & 3; + if( ( param.ByteOffset & 0x3 ) != 0 || ( param.ByteSize & 0x3 ) != 0 ) + { + MalformedParameters.Add( $"ID: 0x{param.Id:X8}, offset: 0x{param.ByteOffset:X4}, size: 0x{param.ByteSize:X4}" ); + continue; + } + + if( iEnd >= numParameters ) + { + MalformedParameters.Add( + $"{MaterialParamRangeName( materialParams?.Name ?? string.Empty, param.ByteOffset >> 2, param.ByteSize >> 2 )} (ID: 0x{param.Id:X8})" ); + continue; + } + + for( var i = iStart; i <= iEnd; ++i ) + { + var end = i == iEnd ? jEnd : 3; + for( var j = i == iStart ? jStart : 0; j <= end; ++j ) + { + var tt = $"{MaterialParamRangeName( materialParams?.Name ?? string.Empty, param.ByteOffset >> 2, param.ByteSize >> 2 ).Item1} (ID: 0x{param.Id:X8})"; + Matrix[ i, j ] = ( $"0x{param.Id:X8}", tt, ( short )idx, 0 ); + } + } + } + + UpdateOrphans( materialParams ); + UpdateColors( materialParams ); + } + + public void UpdateOrphanStart( int orphanStart ) + { + var oldEnd = Orphans.Count > 0 ? Orphans[ NewMaterialParamEnd ].Index : -1; + UpdateOrphanStart( orphanStart, oldEnd ); + } + + private void UpdateOrphanStart( int orphanStart, int oldEnd ) + { + var count = Math.Min( NewMaterialParamEnd - NewMaterialParamStart + orphanStart + 1, Orphans.Count ); + NewMaterialParamStart = ( short )orphanStart; + var current = Orphans[ NewMaterialParamStart ].Index; + for( var i = NewMaterialParamStart; i < count; ++i ) + { + var next = Orphans[ i ].Index; + if( current++ != next ) + { + NewMaterialParamEnd = ( short )( i - 1 ); + return; + } + + if( next == oldEnd ) + { + NewMaterialParamEnd = i; + return; + } + } + + NewMaterialParamEnd = ( short )( count - 1 ); + } + + private void UpdateOrphans( ShpkFile.Resource? materialParams ) + { + var oldStart = Orphans.Count > 0 ? Orphans[ NewMaterialParamStart ].Index : -1; + var oldEnd = Orphans.Count > 0 ? Orphans[ NewMaterialParamEnd ].Index : -1; + + Orphans.Clear(); + short newMaterialParamStart = 0; + for( var i = 0; i < Matrix.GetLength( 0 ); ++i ) + for( var j = 0; j < 4; ++j ) + { + if( !Matrix[ i, j ].Name.IsNullOrEmpty() ) + { + continue; + } + + Matrix[ i, j ] = ( "(none)", string.Empty, -1, 0 ); + var linear = ( short )( 4 * i + j ); + if( oldStart == linear ) + { + newMaterialParamStart = ( short )Orphans.Count; + } + + Orphans.Add( ( $"{materialParams?.Name ?? string.Empty}{MaterialParamName( false, linear )}", linear ) ); + } + + if( Orphans.Count == 0 ) + { + return; + } + + UpdateOrphanStart( newMaterialParamStart, oldEnd ); + } + + private void UpdateColors( ShpkFile.Resource? materialParams ) + { + var lastIndex = -1; + for( var i = 0; i < Matrix.GetLength( 0 ); ++i ) + { + var usedComponents = ( materialParams?.Used?[ i ] ?? DisassembledShader.VectorComponents.All ) | ( materialParams?.UsedDynamically ?? 0 ); + for( var j = 0; j < 4; ++j ) + { + var color = ( ( byte )usedComponents & ( 1 << j ) ) != 0 + ? ColorType.Used + : 0; + if( Matrix[ i, j ].Index == lastIndex || Matrix[ i, j ].Index < 0 ) + { + color |= ColorType.Continuation; + } + + lastIndex = Matrix[ i, j ].Index; + Matrix[ i, j ].Color = color; + } + } + } + + public bool Valid + => Shpk.Valid; + + public byte[] Write() + => Shpk.Write(); + } +} \ No newline at end of file diff --git a/Penumbra/UI/Classes/ModEditWindow.cs b/Penumbra/UI/Classes/ModEditWindow.cs index 9b0aca95..2c43cca0 100644 --- a/Penumbra/UI/Classes/ModEditWindow.cs +++ b/Penumbra/UI/Classes/ModEditWindow.cs @@ -580,11 +580,11 @@ public partial class ModEditWindow : Window, IDisposable DrawModelPanel, () => _mod?.ModPath.FullName ?? string.Empty, null ); - _shaderPackageTab = new FileEditor< ShpkFile >( "Shader Packages", ".shpk", + _shaderPackageTab = new FileEditor< ShpkTab >( "Shader Packages", ".shpk", () => _editor?.ShpkFiles ?? Array.Empty< Editor.FileRegistry >(), DrawShaderPackagePanel, () => _mod?.ModPath.FullName ?? string.Empty, - bytes => new ShpkFile( bytes, true ) ); + bytes => new ShpkTab( bytes ) ); _center = new CombinedTexture( _left, _right ); }