Mtrl shader resource editing, ShPk editing

This commit is contained in:
Exter-N 2023-02-15 02:07:10 +01:00 committed by Ottermandias
parent 7ee80c7d48
commit 0c17892f03
12 changed files with 2535 additions and 63 deletions

View file

@ -11,6 +11,7 @@ using OtterGui.Raii;
using Penumbra.GameData.Files;
using Penumbra.Mods;
using Penumbra.String.Classes;
using SixLabors.ImageSharp.PixelFormats;
namespace Penumbra.UI.Classes;
@ -23,6 +24,7 @@ public partial class ModEditWindow
private readonly Func< IReadOnlyList< Mod.Editor.FileRegistry > > _getFiles;
private readonly Func< T, bool, bool > _drawEdit;
private readonly Func< string > _getInitialPath;
private readonly Func< byte[], T? > _parseFile;
private Mod.Editor.FileRegistry? _currentPath;
private T? _currentFile;
@ -39,13 +41,14 @@ public partial class ModEditWindow
private readonly FileDialogManager _fileDialog = ConfigWindow.SetupFileManager();
public FileEditor( string tabName, string fileType, Func< IReadOnlyList< Mod.Editor.FileRegistry > > getFiles,
Func< T, bool, bool > drawEdit, Func< string > getInitialPath )
Func< T, bool, bool > drawEdit, Func< string > getInitialPath, Func< byte[], T? >? parseFile )
{
_tabName = tabName;
_fileType = fileType;
_getFiles = getFiles;
_drawEdit = drawEdit;
_getInitialPath = getInitialPath;
_parseFile = parseFile ?? DefaultParseFile;
}
public void Draw()
@ -84,7 +87,7 @@ public partial class ModEditWindow
if( file != null )
{
_defaultException = null;
_defaultFile = Activator.CreateInstance( typeof( T ), file.Data ) as T;
_defaultFile = _parseFile( file.Data );
}
else
{
@ -172,6 +175,11 @@ public partial class ModEditWindow
}
}
private static T? DefaultParseFile( byte[] bytes )
{
return Activator.CreateInstance( typeof( T ), bytes ) as T;
}
private void UpdateCurrentFile( Mod.Editor.FileRegistry path )
{
if( ReferenceEquals( _currentPath, path ) )
@ -185,7 +193,7 @@ public partial class ModEditWindow
try
{
var bytes = File.ReadAllBytes( _currentPath.File.FullName );
_currentFile = Activator.CreateInstance( typeof( T ), bytes ) as T;
_currentFile = _parseFile( bytes );
}
catch( Exception e )
{

View file

@ -1,15 +1,23 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Numerics;
using System.Runtime.InteropServices;
using Dalamud.Interface;
using Dalamud.Interface.ImGuiFileDialog;
using Dalamud.Interface.Internal.Notifications;
using ImGuiNET;
using Lumina.Data.Parsing;
using Newtonsoft.Json.Linq;
using OtterGui;
using OtterGui.Raii;
using Penumbra.GameData.Files;
using Penumbra.String.Classes;
using Penumbra.String.Functions;
using Penumbra.Util;
using static OtterGui.Raii.ImRaii;
namespace Penumbra.UI.Classes;
@ -17,7 +25,12 @@ public partial class ModEditWindow
{
private readonly FileEditor< MtrlFile > _materialTab;
private static bool DrawMaterialPanel( MtrlFile file, bool disabled )
private readonly FileDialogManager _materialFileDialog = ConfigWindow.SetupFileManager();
private uint _materialNewConstantId = 0;
private uint _materialNewSamplerId = 0;
private bool DrawMaterialPanel( MtrlFile file, bool disabled )
{
var ret = DrawMaterialTextureChange( file, disabled );
@ -27,22 +40,38 @@ public partial class ModEditWindow
ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() / 2 ) );
ret |= DrawMaterialColorSetChange( file, disabled );
ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() / 2 ) );
ret |= DrawMaterialShaderResources( file, disabled );
ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() / 2 ) );
ret |= DrawOtherMaterialDetails( file, disabled );
_materialFileDialog.Draw();
return !disabled && ret;
}
private static bool DrawMaterialTextureChange( MtrlFile file, bool disabled )
{
var samplers = file.GetSamplersByTexture();
var names = new List<string>();
var maxWidth = 0.0f;
for( var i = 0; i < file.Textures.Length; ++i )
{
var (sampler, shpkSampler) = samplers[i];
var name = shpkSampler.HasValue ? shpkSampler.Value.Name : sampler.HasValue ? $"0x{sampler.Value.SamplerId:X8}" : $"#{i}";
names.Add( name );
maxWidth = Math.Max( maxWidth, ImGui.CalcTextSize( name ).X );
}
using var id = ImRaii.PushId( "Textures" );
var ret = false;
for( var i = 0; i < file.Textures.Length; ++i )
{
using var _ = ImRaii.PushId( i );
var tmp = file.Textures[ i ].Path;
ImGui.SetNextItemWidth( ImGui.GetContentRegionAvail().X );
if( ImGui.InputText( string.Empty, ref tmp, Utf8GamePath.MaxGamePathLength,
ImGui.SetNextItemWidth( ImGui.GetContentRegionAvail().X - maxWidth );
if( ImGui.InputText( names[i], ref tmp, Utf8GamePath.MaxGamePathLength,
disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None )
&& tmp.Length > 0
&& tmp != file.Textures[ i ].Path )
@ -140,24 +169,350 @@ public partial class ModEditWindow
return ret;
}
private static bool DrawOtherMaterialDetails( MtrlFile file, bool _ )
private bool DrawMaterialShaderResources( MtrlFile file, bool disabled )
{
if( !ImGui.CollapsingHeader( "Further Content" ) )
var ret = false;
if( !ImGui.CollapsingHeader( "Advanced Shader Resources" ) )
{
return false;
}
using( var textures = ImRaii.TreeNode( "Textures", ImGuiTreeNodeFlags.DefaultOpen ) )
ImGui.SetNextItemWidth( ImGuiHelpers.GlobalScale * 150.0f );
if( ImGui.InputText( "Shader Package Name", ref file.ShaderPackage.Name, 63, disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None ) )
{
if( textures )
ret = true;
}
var shpkFlags = ( int )file.ShaderPackage.Flags;
ImGui.SetNextItemWidth( ImGuiHelpers.GlobalScale * 150.0f );
if( ImGui.InputInt( "Shader Package Flags", ref shpkFlags, 0, 0, ImGuiInputTextFlags.CharsHexadecimal | ( disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None ) ) )
{
file.ShaderPackage.Flags = ( uint )shpkFlags;
ret = true;
}
ImRaii.TreeNode( $"Has associated ShPk file (for advanced editing): {( file.AssociatedShpk != null ? "Yes" : "No" )}", ImGuiTreeNodeFlags.Leaf ).Dispose();
if( !disabled && ImGui.Button( "Associate modded ShPk file" ) )
{
_materialFileDialog.OpenFileDialog( $"Associate modded ShPk file...", ".shpk", ( success, name ) =>
{
foreach( var tex in file.Textures )
if( !success )
{
ImRaii.TreeNode( $"{tex.Path} - {tex.Flags:X4}", ImGuiTreeNodeFlags.Leaf ).Dispose();
return;
}
try
{
file.AssociatedShpk = new ShpkFile( File.ReadAllBytes( name ) );
}
catch( Exception e )
{
Penumbra.Log.Error( $"Could not load ShPk file {name}:\n{e}" );
ChatUtil.NotificationMessage( $"Could not load {Path.GetFileName( name )}:\n{e.Message}", "Penumbra Advanced Editing", NotificationType.Error );
return;
}
ChatUtil.NotificationMessage( $"Advanced Shader Resources for this material will now be based on the supplied {Path.GetFileName( name )}", "Penumbra Advanced Editing", NotificationType.Success );
} );
}
if( file.ShaderPackage.ShaderKeys.Length > 0 )
{
using var t = ImRaii.TreeNode( "Shader Keys" );
if( t )
{
foreach( var (key, idx) in file.ShaderPackage.ShaderKeys.WithIndex() )
{
using var t2 = ImRaii.TreeNode( $"Shader Key #{idx}", file.ShaderPackage.ShaderKeys.Length == 1 ? ImGuiTreeNodeFlags.DefaultOpen : 0 );
if( t2 )
{
ImRaii.TreeNode( $"Category: 0x{key.Category:X8} ({key.Category})", ImGuiTreeNodeFlags.Leaf ).Dispose();
ImRaii.TreeNode( $"Value: 0x{key.Value:X8} ({key.Value})", ImGuiTreeNodeFlags.Leaf ).Dispose();
}
}
}
}
if( file.ShaderPackage.Constants.Length > 0 || file.ShaderPackage.ShaderValues.Length > 0
|| file.AssociatedShpk != null && file.AssociatedShpk.Constants.Length > 0 )
{
var materialParams = file.AssociatedShpk?.GetConstantById( ShpkFile.MaterialParamsConstantId );
using var t = ImRaii.TreeNode( materialParams?.Name ?? "Constants" );
if( t )
{
var orphanValues = new IndexSet( file.ShaderPackage.ShaderValues.Length, true );
var aliasedValueCount = 0;
var definedConstants = new HashSet< uint >();
var hasMalformedConstants = false;
foreach( var constant in file.ShaderPackage.Constants )
{
definedConstants.Add( constant.Id );
var values = file.GetConstantValues( constant );
if( file.GetConstantValues( constant ).Length > 0 )
{
var unique = orphanValues.RemoveRange( constant.ByteOffset >> 2, values.Length );
aliasedValueCount += values.Length - unique;
}
else
{
hasMalformedConstants = true;
}
}
foreach( var (constant, idx) in file.ShaderPackage.Constants.WithIndex() )
{
var values = file.GetConstantValues( constant );
var paramValueOffset = -values.Length;
if( values.Length > 0 )
{
var shpkParam = file.AssociatedShpk?.GetMaterialParamById( constant.Id );
var paramByteOffset = shpkParam.HasValue ? shpkParam.Value.ByteOffset : -1;
if( ( paramByteOffset & 0x3 ) == 0 )
{
paramValueOffset = paramByteOffset >> 2;
}
}
var (constantName, componentOnly) = MaterialParamRangeName( materialParams?.Name ?? "", paramValueOffset, values.Length );
using var t2 = ImRaii.TreeNode( $"#{idx}{( constantName != null ? ( ": " + constantName ) : "" )} (ID: 0x{constant.Id:X8})" );
if( t2 )
{
if( values.Length > 0 )
{
var valueOffset = constant.ByteOffset >> 2;
for( var valueIdx = 0; valueIdx < values.Length; ++valueIdx )
{
ImGui.SetNextItemWidth( ImGuiHelpers.GlobalScale * 150.0f );
if( ImGui.InputFloat( $"{MaterialParamName( componentOnly, paramValueOffset + valueIdx ) ?? $"#{valueIdx}"} (at 0x{( ( valueOffset + valueIdx ) << 2 ):X4})",
ref values[valueIdx], 0.0f, 0.0f, "%.3f",
disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None ) )
{
ret = true;
}
}
}
else
{
ImRaii.TreeNode( $"Offset: 0x{constant.ByteOffset:X4}", ImGuiTreeNodeFlags.Leaf ).Dispose();
ImRaii.TreeNode( $"Size: 0x{constant.ByteSize:X4}", ImGuiTreeNodeFlags.Leaf ).Dispose();
}
if( !disabled && !hasMalformedConstants && orphanValues.Count == 0 && aliasedValueCount == 0
&& ImGui.Button( "Remove Constant" ) )
{
ArrayRemove( ref file.ShaderPackage.ShaderValues, constant.ByteOffset >> 2, constant.ByteSize >> 2 );
ArrayRemove( ref file.ShaderPackage.Constants, idx );
for( var i = 0; i < file.ShaderPackage.Constants.Length; ++i )
{
if( file.ShaderPackage.Constants[i].ByteOffset >= constant.ByteOffset )
{
file.ShaderPackage.Constants[i].ByteOffset -= constant.ByteSize;
}
}
ret = true;
}
}
}
if( orphanValues.Count > 0 )
{
using var t2 = ImRaii.TreeNode( $"Orphan Values ({orphanValues.Count})" );
if( t2 )
{
foreach( var idx in orphanValues )
{
ImGui.SetNextItemWidth( ImGui.GetFontSize() * 10.0f );
if( ImGui.InputFloat( $"#{idx} (at 0x{( idx << 2 ):X4})",
ref file.ShaderPackage.ShaderValues[idx], 0.0f, 0.0f, "%.3f",
disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None ) )
{
ret = true;
}
}
}
}
else if ( !disabled && !hasMalformedConstants && file.AssociatedShpk != null )
{
var missingConstants = file.AssociatedShpk.MaterialParams.Where( constant => ( constant.ByteOffset & 0x3 ) == 0 && ( constant.ByteSize & 0x3 ) == 0 && !definedConstants.Contains( constant.Id ) ).ToArray();
if( missingConstants.Length > 0 )
{
var selectedConstant = Array.Find( missingConstants, constant => constant.Id == _materialNewConstantId );
if( selectedConstant.ByteSize == 0 )
{
selectedConstant = missingConstants[0];
_materialNewConstantId = selectedConstant.Id;
}
ImGui.SetNextItemWidth( ImGuiHelpers.GlobalScale * 450.0f );
var (selectedConstantName, _) = MaterialParamRangeName( materialParams?.Name ?? "", selectedConstant.ByteOffset >> 2, selectedConstant.ByteSize >> 2 );
using( var c = ImRaii.Combo( "##NewConstantId", $"{selectedConstantName} (ID: 0x{selectedConstant.Id:X8})" ) )
{
if( c )
{
foreach( var constant in missingConstants )
{
var (constantName, _) = MaterialParamRangeName( materialParams?.Name ?? "", constant.ByteOffset >> 2, constant.ByteSize >> 2 );
if( ImGui.Selectable( $"{constantName} (ID: 0x{constant.Id:X8})" ) )
{
selectedConstant = constant;
_materialNewConstantId = constant.Id;
}
}
}
}
ImGui.SameLine();
if( ImGui.Button( "Add Constant" ) )
{
var valueOffset = ArrayAdd( ref file.ShaderPackage.ShaderValues, 0.0f, selectedConstant.ByteSize >> 2 );
ArrayAdd( ref file.ShaderPackage.Constants, new MtrlFile.Constant
{
Id = _materialNewConstantId,
ByteOffset = ( ushort )( valueOffset << 2 ),
ByteSize = selectedConstant.ByteSize,
} );
ret = true;
}
}
}
}
}
if( file.ShaderPackage.Samplers.Length > 0 || file.Textures.Length > 0
|| file.AssociatedShpk != null && file.AssociatedShpk.Samplers.Any( sampler => sampler.Slot == 2 ) )
{
using var t = ImRaii.TreeNode( "Samplers" );
if( t )
{
var orphanTextures = new IndexSet( file.Textures.Length, true );
var aliasedTextureCount = 0;
var definedSamplers = new HashSet< uint >();
foreach( var sampler in file.ShaderPackage.Samplers )
{
if( !orphanTextures.Remove( sampler.TextureIndex ) )
{
++aliasedTextureCount;
}
definedSamplers.Add( sampler.SamplerId );
}
foreach( var (sampler, idx) in file.ShaderPackage.Samplers.WithIndex() )
{
var shpkSampler = file.AssociatedShpk?.GetSamplerById( sampler.SamplerId );
using var t2 = ImRaii.TreeNode( $"#{idx}{( shpkSampler.HasValue ? ( ": " + shpkSampler.Value.Name ) : "" )} (ID: 0x{sampler.SamplerId:X8})" );
if( t2 )
{
ImRaii.TreeNode( $"Texture: #{sampler.TextureIndex} - {Path.GetFileName( file.Textures[sampler.TextureIndex].Path )}", ImGuiTreeNodeFlags.Leaf ).Dispose();
// FIXME this probably doesn't belong here
static unsafe bool InputHexUInt16( string label, ref ushort v, ImGuiInputTextFlags flags )
{
fixed( ushort* v2 = &v )
{
return ImGui.InputScalar( label, ImGuiDataType.U16, new nint( v2 ), nint.Zero, nint.Zero, "%04X", flags );
}
}
ImGui.SetNextItemWidth( ImGuiHelpers.GlobalScale * 150.0f );
if( InputHexUInt16( "Texture Flags", ref file.Textures[sampler.TextureIndex].Flags, disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None ) )
{
ret = true;
}
var sampFlags = ( int )sampler.Flags;
ImGui.SetNextItemWidth( ImGuiHelpers.GlobalScale * 150.0f );
if( ImGui.InputInt( "Sampler Flags", ref sampFlags, 0, 0, ImGuiInputTextFlags.CharsHexadecimal | ( disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None ) ) )
{
file.ShaderPackage.Samplers[idx].Flags = ( uint )sampFlags;
ret = true;
}
if( !disabled && orphanTextures.Count == 0 && aliasedTextureCount == 0
&& ImGui.Button( "Remove Sampler" ) )
{
ArrayRemove( ref file.Textures, sampler.TextureIndex );
ArrayRemove( ref file.ShaderPackage.Samplers, idx );
for( var i = 0; i < file.ShaderPackage.Samplers.Length; ++i )
{
if( file.ShaderPackage.Samplers[i].TextureIndex >= sampler.TextureIndex )
{
--file.ShaderPackage.Samplers[i].TextureIndex;
}
}
ret = true;
}
}
}
if( orphanTextures.Count > 0 )
{
using var t2 = ImRaii.TreeNode( $"Orphan Textures ({orphanTextures.Count})" );
if( t2 )
{
foreach( var idx in orphanTextures )
{
ImRaii.TreeNode( $"#{idx}: {Path.GetFileName( file.Textures[idx].Path )} - {file.Textures[idx].Flags:X4}", ImGuiTreeNodeFlags.Leaf ).Dispose();
}
}
}
else if( !disabled && file.AssociatedShpk != null && aliasedTextureCount == 0 && file.Textures.Length < 255 )
{
var missingSamplers = file.AssociatedShpk.Samplers.Where( sampler => sampler.Slot == 2 && !definedSamplers.Contains( sampler.Id ) ).ToArray();
if( missingSamplers.Length > 0 )
{
var selectedSampler = Array.Find( missingSamplers, sampler => sampler.Id == _materialNewSamplerId );
if( selectedSampler.Name == null )
{
selectedSampler = missingSamplers[0];
_materialNewSamplerId = selectedSampler.Id;
}
ImGui.SetNextItemWidth( ImGuiHelpers.GlobalScale * 450.0f );
using( var c = ImRaii.Combo( "##NewSamplerId", $"{selectedSampler.Name} (ID: 0x{selectedSampler.Id:X8})" ) )
{
if( c )
{
foreach( var sampler in missingSamplers )
{
if( ImGui.Selectable( $"{sampler.Name} (ID: 0x{sampler.Id:X8})" ) )
{
selectedSampler = sampler;
_materialNewSamplerId = sampler.Id;
}
}
}
}
ImGui.SameLine();
if( ImGui.Button( "Add Sampler" ) )
{
var texIndex = ArrayAdd( ref file.Textures, new MtrlFile.Texture
{
Path = string.Empty,
Flags = 0,
} );
ArrayAdd( ref file.ShaderPackage.Samplers, new Sampler
{
SamplerId = _materialNewSamplerId,
TextureIndex = ( byte )texIndex,
Flags = 0,
} );
ret = true;
}
}
}
}
}
return ret;
}
private bool DrawOtherMaterialDetails( MtrlFile file, bool disabled )
{
var ret = false;
if( !ImGui.CollapsingHeader( "Further Content" ) )
{
return false;
}
using( var sets = ImRaii.TreeNode( "UV Sets", ImGuiTreeNodeFlags.DefaultOpen ) )
{
if( sets )
@ -169,50 +524,6 @@ public partial class ModEditWindow
}
}
using( var shaders = ImRaii.TreeNode( "Shaders", ImGuiTreeNodeFlags.DefaultOpen ) )
{
if( shaders )
{
ImRaii.TreeNode( $"Name: {file.ShaderPackage.Name}", ImGuiTreeNodeFlags.Leaf ).Dispose();
ImRaii.TreeNode( $"Flags: {file.ShaderPackage.Flags:X8}", ImGuiTreeNodeFlags.Leaf ).Dispose();
foreach( var (key, idx) in file.ShaderPackage.ShaderKeys.WithIndex() )
{
using var t = ImRaii.TreeNode( $"Shader Key #{idx}" );
if( t )
{
ImRaii.TreeNode( $"Category: {key.Category}", ImGuiTreeNodeFlags.Leaf ).Dispose();
ImRaii.TreeNode( $"Value: {key.Value}", ImGuiTreeNodeFlags.Leaf ).Dispose();
}
}
foreach( var (constant, idx) in file.ShaderPackage.Constants.WithIndex() )
{
using var t = ImRaii.TreeNode( $"Constant #{idx}" );
if( t )
{
ImRaii.TreeNode( $"Category: {constant.Id}", ImGuiTreeNodeFlags.Leaf ).Dispose();
ImRaii.TreeNode( $"Value: 0x{constant.Value:X8}", ImGuiTreeNodeFlags.Leaf ).Dispose();
}
}
foreach( var (sampler, idx) in file.ShaderPackage.Samplers.WithIndex() )
{
using var t = ImRaii.TreeNode( $"Sampler #{idx}" );
if( t )
{
ImRaii.TreeNode( $"ID: {sampler.SamplerId}", ImGuiTreeNodeFlags.Leaf ).Dispose();
ImRaii.TreeNode( $"Texture Index: {sampler.TextureIndex}", ImGuiTreeNodeFlags.Leaf ).Dispose();
ImRaii.TreeNode( $"Flags: 0x{sampler.Flags:X8}", ImGuiTreeNodeFlags.Leaf ).Dispose();
}
}
foreach( var (value, idx) in file.ShaderPackage.ShaderValues.WithIndex() )
{
ImRaii.TreeNode( $"Value #{idx}: {value.ToString( CultureInfo.InvariantCulture )}", ImGuiTreeNodeFlags.Leaf ).Dispose();
}
}
}
if( file.AdditionalData.Length > 0 )
{
using var t = ImRaii.TreeNode( $"Additional Data (Size: {file.AdditionalData.Length})###AdditionalData" );
@ -222,7 +533,7 @@ public partial class ModEditWindow
}
}
return false;
return ret;
}
private static void ColorSetCopyAllClipboardButton( MtrlFile file, int colorSetIdx )
@ -659,4 +970,69 @@ public partial class ModEditWindow
}
}
}
// FIXME this probably doesn't belong here
// Also used in ShaderPackages
private static int ArrayAdd<T>( ref T[] array, T element, int count = 1 )
{
var length = array.Length;
var newArray = new T[array.Length + count];
Array.Copy( array, newArray, length );
for( var i = 0; i < count; ++i )
{
newArray[length + i] = element;
}
array = newArray;
return length;
}
private static void ArrayRemove<T>( ref T[] array, int offset, int count = 1 )
{
var newArray = new T[array.Length - count];
Array.Copy( array, newArray, offset );
Array.Copy( array, offset + count, newArray, offset, newArray.Length - offset );
array = newArray;
}
private static (string?, bool) MaterialParamRangeName( string prefix, int valueOffset, int valueLength )
{
if( valueLength == 0 || valueOffset < 0 )
{
return (null, false);
}
var firstVector = valueOffset >> 2;
var lastVector = ( valueOffset + valueLength - 1 ) >> 2;
var firstComponent = valueOffset & 0x3;
var lastComponent = ( valueOffset + valueLength - 1 ) & 0x3;
static string VectorSwizzle( int firstComponent, int numComponents )
=> ( numComponents == 4 ) ? "" : string.Concat( ".", "xyzw".AsSpan( firstComponent, numComponents ) );
if( firstVector == lastVector )
{
return ($"{prefix}[{firstVector}]{VectorSwizzle( firstComponent, lastComponent + 1 - firstComponent )}", true);
}
var parts = new string[lastVector + 1 - firstVector];
parts[0] = $"{prefix}[{firstVector}]{VectorSwizzle( firstComponent, 4 - firstComponent )}";
parts[^1] = $"[{lastVector}]{VectorSwizzle( 0, lastComponent + 1 )}";
for( var i = firstVector + 1; i < lastVector; ++i )
{
parts[i - firstVector] = $"[{i}]";
}
return (string.Join( ", ", parts ), false);
}
private static string? MaterialParamName( bool componentOnly, int offset )
{
if( offset < 0 )
{
return null;
}
var component = "xyzw"[offset & 0x3];
return componentOnly ? new string( component, 1 ) : $"[{offset >> 2}].{component}";
}
}

View file

@ -0,0 +1,557 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Numerics;
using Dalamud.Interface.ImGuiFileDialog;
using Dalamud.Interface.Internal.Notifications;
using Dalamud.Interface;
using ImGuiNET;
using OtterGui.Raii;
using OtterGui;
using Penumbra.GameData.Data;
using Penumbra.GameData.Files;
using Penumbra.Util;
using Lumina.Data.Parsing;
using static OtterGui.Raii.ImRaii;
namespace Penumbra.UI.Classes;
public partial class ModEditWindow
{
private readonly FileEditor<ShpkFile> _shaderPackageTab;
private readonly FileDialogManager _shaderPackageFileDialog = ConfigWindow.SetupFileManager();
private uint _shaderPackageNewMaterialParamId = 0;
private ushort _shaderPackageNewMaterialParamStart = 0;
private ushort _shaderPackageNewMaterialParamEnd = 0;
private bool DrawShaderPackagePanel( ShpkFile file, bool disabled )
{
var ret = DrawShaderPackageSummary( file, 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 );
ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() / 2 ) );
ret |= DrawShaderPackageMaterialParamLayout( file, disabled );
ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() / 2 ) );
ret |= DrawOtherShaderPackageDetails( file, disabled );
_shaderPackageFileDialog.Draw();
ret |= file.IsChanged();
return !disabled && ret;
}
private static bool DrawShaderPackageSummary( ShpkFile file, bool _ )
{
ImGui.Text( $"Shader Package for DirectX {( int )file.DirectXVersion}" );
return false;
}
private bool DrawShaderPackageShaderArray( string objectName, ShpkFile.Shader[] shaders, ShpkFile file, bool disabled )
{
if( shaders.Length == 0 )
{
return false;
}
if( !ImGui.CollapsingHeader( $"{objectName}s" ) )
{
return false;
}
var ret = false;
foreach( var (shader, idx) in shaders.WithIndex() )
{
using var t = ImRaii.TreeNode( $"{objectName} #{idx}" );
if( t )
{
if( ImGui.Button( $"Export Shader Blob ({shader.Blob.Length} bytes)" ) )
{
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} 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 Blob {defaultName}{extension} exported successfully to {Path.GetFileName( name )}", "Penumbra Advanced Editing", NotificationType.Success );
} );
}
if( !disabled )
{
ImGui.SameLine();
if( ImGui.Button( "Replace Shader Blob" ) )
{
_shaderPackageFileDialog.OpenFileDialog( $"Replace {objectName} #{idx} Blob...", "Shader 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, disabled );
ret |= DrawShaderPackageResourceArray( "Samplers", "slot", false, shader.Samplers, disabled );
ret |= DrawShaderPackageResourceArray( "Unknown Type X Resources", "slot", true, shader.UnknownX, disabled );
ret |= DrawShaderPackageResourceArray( "Unknown Type Y Resources", "slot", true, shader.UnknownY, disabled );
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 Disassembly" ) )
{
if( t2 )
{
using( var font = ImRaii.PushFont( UiBuilder.MonoFont ) )
{
ImGui.TextUnformatted( shader.Disassembly!.RawDisassembly );
}
}
}
}
}
return ret;
}
private bool DrawShaderPackageMaterialParamLayout( ShpkFile file, bool disabled )
{
var ret = false;
var materialParams = file.GetConstantById( ShpkFile.MaterialParamsConstantId );
if( !ImGui.CollapsingHeader( $"{materialParams?.Name ?? "Material Parameter"} Layout" ) )
{
return false;
}
var isSizeWellDefined = ( file.MaterialParamsSize & 0xF ) == 0 && ( !materialParams.HasValue || file.MaterialParamsSize == ( materialParams.Value.Size << 4 ) );
if( !isSizeWellDefined )
{
if( materialParams.HasValue )
{
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" );
}
}
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 )
{
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;
}
}
ImGui.Text( "Parameter positions (continuations are grayed out, unused values are red):" );
using( var table = ImRaii.Table( "##MaterialParamLayout", 5,
ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg ) )
{
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;
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();
}
}
}
if( hasMalformedParameters )
{
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" ) )
{
ArrayRemove( ref file.MaterialParams, 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}" ) )
{
_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}" ) )
{
_shaderPackageNewMaterialParamEnd = ( ushort )end;
}
}
}
}
var id = ( int )_shaderPackageNewMaterialParamId;
ImGui.SetNextItemWidth( ImGuiHelpers.GlobalScale * 150.0f );
if( ImGui.InputInt( "ID", ref id, 0, 0, ImGuiInputTextFlags.CharsHexadecimal ) )
{
_shaderPackageNewMaterialParamId = ( uint )id;
}
if( ImGui.Button( "Add" ) )
{
if( definedParameters.Contains( _shaderPackageNewMaterialParamId ) )
{
ChatUtil.NotificationMessage( $"Duplicate parameter ID 0x{_shaderPackageNewMaterialParamId:X8}", "Penumbra Advanced Editing", NotificationType.Error );
}
else
{
ArrayAdd( ref file.MaterialParams, new ShpkFile.MaterialParam
{
Id = _shaderPackageNewMaterialParamId,
ByteOffset = ( ushort )( _shaderPackageNewMaterialParamStart << 2 ),
ByteSize = ( ushort )( ( _shaderPackageNewMaterialParamEnd + 1 - _shaderPackageNewMaterialParamStart ) << 2 ),
} );
ret = true;
}
}
}
}
}
}
return ret;
}
private static bool DrawShaderPackageResourceArray( string arrayName, string slotLabel, bool withSize, ShpkFile.Resource[] resources, bool _ )
{
if( resources.Length == 0 )
{
return false;
}
using var t = ImRaii.TreeNode( arrayName );
if( !t )
{
return false;
}
var ret = false;
foreach( var (buf, idx) in resources.WithIndex() )
{
using var t2 = ImRaii.TreeNode( $"#{idx}: {buf.Name} (ID: 0x{buf.Id:X8}), {slotLabel}: {buf.Slot}" + ( withSize ? $", size: {buf.Size} registers" : string.Empty ), ( buf.Used != null ) ? 0 : ImGuiTreeNodeFlags.Leaf );
if( t2 )
{
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();
}
}
}
return ret;
}
private static bool DrawOtherShaderPackageDetails( ShpkFile file, bool disabled )
{
var ret = false;
if( !ImGui.CollapsingHeader( "Further Content" ) )
{
return false;
}
ret |= DrawShaderPackageResourceArray( "Constant Buffers", "type", true, file.Constants, disabled );
ret |= DrawShaderPackageResourceArray( "Samplers", "type", false, file.Samplers, disabled );
if( file.UnknownA.Length > 0 )
{
using var t = ImRaii.TreeNode( $"Unknown Type A Structures ({file.UnknownA.Length})" );
if( t )
{
foreach( var (unk, idx) in file.UnknownA.WithIndex() )
{
ImRaii.TreeNode( $"#{idx}: 0x{unk.Item1:X8}, 0x{unk.Item2:X8}", ImGuiTreeNodeFlags.Leaf ).Dispose();
}
}
}
if( file.UnknownB.Length > 0 )
{
using var t = ImRaii.TreeNode( $"Unknown Type B Structures ({file.UnknownB.Length})" );
if( t )
{
foreach( var (unk, idx) in file.UnknownB.WithIndex() )
{
ImRaii.TreeNode( $"#{idx}: 0x{unk.Item1:X8}, 0x{unk.Item2:X8}", ImGuiTreeNodeFlags.Leaf ).Dispose();
}
}
}
if( file.UnknownC.Length > 0 )
{
using var t = ImRaii.TreeNode( $"Unknown Type C Structures ({file.UnknownC.Length})" );
if( t )
{
foreach( var (unk, idx) in file.UnknownC.WithIndex() )
{
ImRaii.TreeNode( $"#{idx}: 0x{unk.Item1:X8}, 0x{unk.Item2:X8}", ImGuiTreeNodeFlags.Leaf ).Dispose();
}
}
}
using( var t = ImRaii.TreeNode( $"Misc. Unknown Fields" ) )
{
if( t )
{
ImRaii.TreeNode( $"#1 (at 0x0004): 0x{file.Unknown1:X8}", ImGuiTreeNodeFlags.Leaf ).Dispose();
ImRaii.TreeNode( $"#2 (at 0x003C): 0x{file.Unknown2:X8}", ImGuiTreeNodeFlags.Leaf ).Dispose();
ImRaii.TreeNode( $"#3 (at 0x0040): 0x{file.Unknown3:X8}", ImGuiTreeNodeFlags.Leaf ).Dispose();
ImRaii.TreeNode( $"#4 (at 0x0044): 0x{file.Unknown4:X8}", 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}" ) ) );
}
}
using( var t = ImRaii.TreeNode( $"String Pool" ) )
{
if( t )
{
foreach( var offset in file.Strings.StartingOffsets )
{
ImGui.Text( file.Strings.GetNullTerminatedString( offset ) );
}
}
}
return ret;
}
}

View file

@ -1,4 +1,5 @@
using System;
using System.IO;
using System.Linq;
using System.Numerics;
using System.Text;
@ -47,6 +48,7 @@ public partial class ModEditWindow : Window, IDisposable
_selectedFiles.Clear();
_modelTab.Reset();
_materialTab.Reset();
_shaderPackageTab.Reset();
_swapWindow.UpdateMod( mod, Penumbra.CollectionManager.Current[ mod.Index ].Settings );
}
@ -155,6 +157,7 @@ public partial class ModEditWindow : Window, IDisposable
_modelTab.Draw();
_materialTab.Draw();
DrawTextureTab();
_shaderPackageTab.Draw();
_swapWindow.DrawItemSwapPanel();
}
@ -532,17 +535,65 @@ public partial class ModEditWindow : Window, IDisposable
ImGui.InputTextWithHint( "##swapValue", "... instead of this file.", ref _newSwapKey, Utf8GamePath.MaxGamePathLength );
}
// FIXME this probably doesn't belong here
private T? LoadAssociatedFile<T>( string gamePath, Func< byte[], T? > parse )
{
var defaultFiles = _mod?.Default?.Files;
if( defaultFiles != null )
{
if( Utf8GamePath.FromString( gamePath, out var utf8Path, true ) )
{
try
{
if (defaultFiles.TryGetValue( utf8Path, out var fsPath ))
{
return parse( File.ReadAllBytes( fsPath.FullName ) );
}
}
finally
{
utf8Path.Dispose();
}
}
}
var file = Dalamud.GameData.GetFile( gamePath )?.Data;
return file == null ? default : parse( file );
}
// FIXME neither does this
private ShpkFile? LoadAssociatedShpk( string shaderName )
{
var path = $"shader/sm5/shpk/{shaderName}";
try
{
return LoadAssociatedFile( path, file => new ShpkFile( file ) );
}
catch( Exception e )
{
Penumbra.Log.Debug( $"Could not parse associated file {path} to Shpk:\n{e}" );
return null;
}
}
public ModEditWindow()
: base( WindowBaseLabel )
{
_materialTab = new FileEditor< MtrlFile >( "Materials", ".mtrl",
() => _editor?.MtrlFiles ?? Array.Empty< Editor.FileRegistry >(),
DrawMaterialPanel,
() => _mod?.ModPath.FullName ?? string.Empty );
() => _mod?.ModPath.FullName ?? string.Empty,
bytes => new MtrlFile( bytes, LoadAssociatedShpk ) );
_modelTab = new FileEditor< MdlFile >( "Models", ".mdl",
() => _editor?.MdlFiles ?? Array.Empty< Editor.FileRegistry >(),
DrawModelPanel,
() => _mod?.ModPath.FullName ?? string.Empty );
() => _mod?.ModPath.FullName ?? string.Empty,
null );
_shaderPackageTab = new FileEditor< ShpkFile >( "Shader Packages", ".shpk",
() => _editor?.ShpkFiles ?? Array.Empty< Editor.FileRegistry >(),
DrawShaderPackagePanel,
() => _mod?.ModPath.FullName ?? string.Empty,
bytes => new ShpkFile( bytes, true ) );
_center = new CombinedTexture( _left, _right );
}