mirror of
https://github.com/xivdev/Penumbra.git
synced 2025-12-12 18:27:24 +01:00
Mtrl shader resource editing, ShPk editing
This commit is contained in:
parent
7ee80c7d48
commit
0c17892f03
12 changed files with 2535 additions and 63 deletions
481
Penumbra.GameData/Data/DisassembledShader.cs
Normal file
481
Penumbra.GameData/Data/DisassembledShader.cs
Normal file
|
|
@ -0,0 +1,481 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using Penumbra.GameData.Interop;
|
||||
using static Penumbra.GameData.Files.ShpkFile;
|
||||
|
||||
namespace Penumbra.GameData.Data;
|
||||
|
||||
public class DisassembledShader
|
||||
{
|
||||
public struct ResourceBinding
|
||||
{
|
||||
public string Name;
|
||||
public ResourceType Type;
|
||||
public Format Format;
|
||||
public ResourceDimension Dimension;
|
||||
public uint Slot;
|
||||
public uint Elements;
|
||||
public uint RegisterCount;
|
||||
public VectorComponents[] Used;
|
||||
public VectorComponents UsedDynamically;
|
||||
}
|
||||
|
||||
// Abbreviated using the uppercased first char of their name
|
||||
public enum ResourceType : byte
|
||||
{
|
||||
Unspecified = 0,
|
||||
ConstantBuffer = 0x43, // 'C'
|
||||
Sampler = 0x53, // 'S'
|
||||
Texture = 0x54, // 'T'
|
||||
UAV = 0x55, // 'U'
|
||||
}
|
||||
|
||||
// Abbreviated using the uppercased first and last char of their name
|
||||
public enum Format : ushort
|
||||
{
|
||||
Unspecified = 0,
|
||||
NotApplicable = 0x4E41, // 'NA'
|
||||
Int = 0x4954, // 'IT'
|
||||
Int4 = 0x4934, // 'I4'
|
||||
Float = 0x4654, // 'FT'
|
||||
Float4 = 0x4634, // 'F4'
|
||||
}
|
||||
|
||||
// Abbreviated using the uppercased first and last char of their name
|
||||
public enum ResourceDimension : ushort
|
||||
{
|
||||
Unspecified = 0,
|
||||
NotApplicable = 0x4E41, // 'NA'
|
||||
TwoD = 0x3244, // '2D'
|
||||
ThreeD = 0x3344, // '3D'
|
||||
Cube = 0x4345, // 'CE'
|
||||
}
|
||||
|
||||
public struct InputOutput
|
||||
{
|
||||
public string Name;
|
||||
public uint Index;
|
||||
public VectorComponents Mask;
|
||||
public uint Register;
|
||||
public string SystemValue;
|
||||
public Format Format;
|
||||
public VectorComponents Used;
|
||||
}
|
||||
|
||||
[Flags]
|
||||
public enum VectorComponents : byte
|
||||
{
|
||||
X = 1,
|
||||
Y = 2,
|
||||
Z = 4,
|
||||
W = 8,
|
||||
All = 15,
|
||||
}
|
||||
|
||||
public enum ShaderStage : byte
|
||||
{
|
||||
Unspecified = 0,
|
||||
Pixel = 0x50, // 'P'
|
||||
Vertex = 0x56, // 'V'
|
||||
}
|
||||
|
||||
private static readonly Regex ResourceBindingSizeRegex = new(@"\s(\w+)(?:\[\d+\])?;\s*//\s*Offset:\s*0\s*Size:\s*(\d+)$", RegexOptions.Multiline | RegexOptions.NonBacktracking);
|
||||
private static readonly Regex SM3ConstantBufferUsageRegex = new(@"c(\d+)(?:\[([^\]]+)\])?(?:\.([wxyz]+))?", RegexOptions.NonBacktracking);
|
||||
private static readonly Regex SM3TextureUsageRegex = new(@"^\s*texld\S*\s+[^,]+,[^,]+,\s*s(\d+)", RegexOptions.NonBacktracking);
|
||||
private static readonly Regex SM5ConstantBufferUsageRegex = new(@"cb(\d+)\[([^\]]+)\]\.([wxyz]+)", RegexOptions.NonBacktracking);
|
||||
private static readonly Regex SM5TextureUsageRegex = new(@"^\s*sample_\S*\s+[^.]+\.([wxyz]+),[^,]+,\s*t(\d+)\.([wxyz]+)", RegexOptions.NonBacktracking);
|
||||
private static readonly char[] Digits = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' };
|
||||
|
||||
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 DisassembledShader(string 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])]);
|
||||
switch (ShaderModel >> 8)
|
||||
{
|
||||
case 3:
|
||||
ParseSM3Header(header, out BufferDefinitions, out ResourceBindings, out InputSignature, out OutputSignature);
|
||||
ParseSM3ResourceUsage(Instructions, ResourceBindings);
|
||||
break;
|
||||
case 5:
|
||||
ParseSM5Header(header, out BufferDefinitions, out ResourceBindings, out InputSignature, out OutputSignature);
|
||||
ParseSM5ResourceUsage(Instructions, ResourceBindings);
|
||||
break;
|
||||
default:
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
|
||||
public ResourceBinding? GetResourceBindingByName(ResourceType type, string name)
|
||||
{
|
||||
return ResourceBindings.Select(binding => new ResourceBinding?(binding)).FirstOrDefault(binding => binding!.Value.Type == type && binding!.Value.Name == name);
|
||||
}
|
||||
|
||||
public ResourceBinding? GetResourceBindingBySlot(ResourceType type, uint slot)
|
||||
{
|
||||
return ResourceBindings.Select(binding => new ResourceBinding?(binding)).FirstOrDefault(binding => binding!.Value.Type == type && binding!.Value.Slot == slot);
|
||||
}
|
||||
|
||||
public static DisassembledShader Disassemble(ReadOnlySpan<byte> shaderBlob)
|
||||
{
|
||||
return new DisassembledShader(D3DCompiler.Disassemble(shaderBlob));
|
||||
}
|
||||
|
||||
private static void ParseSM3Header(Dictionary<string, string[]> header, out string bufferDefinitions, out ResourceBinding[] resourceBindings, out InputOutput[] inputSignature, out InputOutput[] outputSignature)
|
||||
{
|
||||
if (header.TryGetValue("Parameters", out var rawParameters))
|
||||
{
|
||||
bufferDefinitions = string.Join('\n', rawParameters);
|
||||
}
|
||||
else
|
||||
{
|
||||
bufferDefinitions = string.Empty;
|
||||
}
|
||||
if (header.TryGetValue("Registers", out var rawRegisters))
|
||||
{
|
||||
var (_, registers) = ParseTable(rawRegisters);
|
||||
resourceBindings = Array.ConvertAll(registers, register =>
|
||||
{
|
||||
var type = (ResourceType)(byte)char.ToUpper(register[1][0]);
|
||||
if (type == ResourceType.Sampler)
|
||||
{
|
||||
type = ResourceType.Texture;
|
||||
}
|
||||
uint size = uint.Parse(register[2]);
|
||||
return new ResourceBinding
|
||||
{
|
||||
Name = register[0],
|
||||
Type = type,
|
||||
Format = Format.Unspecified,
|
||||
Dimension = ResourceDimension.Unspecified,
|
||||
Slot = uint.Parse(register[1][1..]),
|
||||
Elements = 1,
|
||||
RegisterCount = size,
|
||||
Used = new VectorComponents[size],
|
||||
};
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
resourceBindings = Array.Empty<ResourceBinding>();
|
||||
}
|
||||
inputSignature = Array.Empty<InputOutput>();
|
||||
outputSignature = Array.Empty<InputOutput>();
|
||||
}
|
||||
|
||||
private static void ParseSM3ResourceUsage(string[] instructions, ResourceBinding[] resourceBindings)
|
||||
{
|
||||
var cbIndices = new Dictionary<uint, int>();
|
||||
var tIndices = new Dictionary<uint, int>();
|
||||
{
|
||||
var i = 0;
|
||||
foreach (var binding in resourceBindings)
|
||||
{
|
||||
switch (binding.Type)
|
||||
{
|
||||
case ResourceType.ConstantBuffer:
|
||||
for (var j = 0u; j < binding.RegisterCount; j++)
|
||||
{
|
||||
cbIndices[binding.Slot + j] = i;
|
||||
}
|
||||
break;
|
||||
case ResourceType.Texture:
|
||||
tIndices[binding.Slot] = i;
|
||||
break;
|
||||
}
|
||||
++i;
|
||||
}
|
||||
}
|
||||
foreach (var instruction in instructions)
|
||||
{
|
||||
var trimmed = instruction.Trim();
|
||||
if (trimmed.StartsWith("def") || trimmed.StartsWith("dcl"))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
foreach (Match cbMatch in SM3ConstantBufferUsageRegex.Matches(instruction))
|
||||
{
|
||||
var buffer = uint.Parse(cbMatch.Groups[1].Value);
|
||||
if (cbIndices.TryGetValue(buffer, out var i))
|
||||
{
|
||||
var swizzle = cbMatch.Groups[3].Success ? ParseVectorComponents(cbMatch.Groups[3].Value) : VectorComponents.All;
|
||||
if (cbMatch.Groups[2].Success)
|
||||
{
|
||||
resourceBindings[i].UsedDynamically |= swizzle;
|
||||
}
|
||||
else
|
||||
{
|
||||
resourceBindings[i].Used[buffer - resourceBindings[i].Slot] |= swizzle;
|
||||
}
|
||||
}
|
||||
}
|
||||
var tMatch = SM3TextureUsageRegex.Match(instruction);
|
||||
if (tMatch.Success)
|
||||
{
|
||||
var texture = uint.Parse(tMatch.Groups[1].Value);
|
||||
if (tIndices.TryGetValue(texture, out var i))
|
||||
{
|
||||
resourceBindings[i].Used[0] = VectorComponents.All;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void ParseSM5Header(Dictionary<string, string[]> header, out string bufferDefinitions, out ResourceBinding[] resourceBindings, out InputOutput[] inputSignature, out InputOutput[] outputSignature)
|
||||
{
|
||||
if (header.TryGetValue("Resource Bindings", out var rawResBindings))
|
||||
{
|
||||
var (head, resBindings) = ParseTable(rawResBindings);
|
||||
resourceBindings = Array.ConvertAll(resBindings, binding => {
|
||||
var type = (ResourceType)(byte)char.ToUpper(binding[1][0]);
|
||||
return new ResourceBinding
|
||||
{
|
||||
Name = binding[0],
|
||||
Type = type,
|
||||
Format = (Format)(((byte)char.ToUpper(binding[2][0]) << 8) | (byte)char.ToUpper(binding[2][^1])),
|
||||
Dimension = (ResourceDimension)(((byte)char.ToUpper(binding[3][0]) << 8) | (byte)char.ToUpper(binding[3][^1])),
|
||||
Slot = uint.Parse(binding[4][binding[4].IndexOfAny(Digits)..]),
|
||||
Elements = uint.Parse(binding[5]),
|
||||
RegisterCount = type == ResourceType.Texture ? 1u : 0u,
|
||||
Used = type == ResourceType.Texture ? new VectorComponents[1] : Array.Empty<VectorComponents>(),
|
||||
};
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
resourceBindings = Array.Empty<ResourceBinding>();
|
||||
}
|
||||
if (header.TryGetValue("Buffer Definitions", out var rawBufferDefs))
|
||||
{
|
||||
bufferDefinitions = string.Join('\n', rawBufferDefs);
|
||||
foreach (Match match in ResourceBindingSizeRegex.Matches(bufferDefinitions))
|
||||
{
|
||||
var name = match.Groups[1].Value;
|
||||
var bytesSize = uint.Parse(match.Groups[2].Value);
|
||||
var pos = Array.FindIndex(resourceBindings, binding => binding.Type == ResourceType.ConstantBuffer && binding.Name == name);
|
||||
if (pos >= 0)
|
||||
{
|
||||
resourceBindings[pos].RegisterCount = (bytesSize + 0xF) >> 4;
|
||||
resourceBindings[pos].Used = new VectorComponents[resourceBindings[pos].RegisterCount];
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
bufferDefinitions = string.Empty;
|
||||
}
|
||||
|
||||
static InputOutput ParseInputOutput(string[] inOut) => new()
|
||||
{
|
||||
Name = inOut[0],
|
||||
Index = uint.Parse(inOut[1]),
|
||||
Mask = ParseVectorComponents(inOut[2]),
|
||||
Register = uint.Parse(inOut[3]),
|
||||
SystemValue = string.Intern(inOut[4]),
|
||||
Format = (Format)(((byte)char.ToUpper(inOut[5][0]) << 8) | (byte)char.ToUpper(inOut[5][^1])),
|
||||
Used = ParseVectorComponents(inOut[6]),
|
||||
};
|
||||
|
||||
if (header.TryGetValue("Input signature", out var rawInputSig))
|
||||
{
|
||||
var (_, inputSig) = ParseTable(rawInputSig);
|
||||
inputSignature = Array.ConvertAll(inputSig, ParseInputOutput);
|
||||
}
|
||||
else
|
||||
{
|
||||
inputSignature = Array.Empty<InputOutput>();
|
||||
}
|
||||
if (header.TryGetValue("Output signature", out var rawOutputSig))
|
||||
{
|
||||
var (_, outputSig) = ParseTable(rawOutputSig);
|
||||
outputSignature = Array.ConvertAll(outputSig, ParseInputOutput);
|
||||
}
|
||||
else
|
||||
{
|
||||
outputSignature = Array.Empty<InputOutput>();
|
||||
}
|
||||
}
|
||||
|
||||
private static void ParseSM5ResourceUsage(string[] instructions, ResourceBinding[] resourceBindings)
|
||||
{
|
||||
var cbIndices = new Dictionary<uint, int>();
|
||||
var tIndices = new Dictionary<uint, int>();
|
||||
{
|
||||
var i = 0;
|
||||
foreach (var binding in resourceBindings)
|
||||
{
|
||||
switch (binding.Type)
|
||||
{
|
||||
case ResourceType.ConstantBuffer:
|
||||
cbIndices[binding.Slot] = i;
|
||||
break;
|
||||
case ResourceType.Texture:
|
||||
tIndices[binding.Slot] = i;
|
||||
break;
|
||||
}
|
||||
++i;
|
||||
}
|
||||
}
|
||||
foreach (var instruction in instructions)
|
||||
{
|
||||
var trimmed = instruction.Trim();
|
||||
if (trimmed.StartsWith("def") || trimmed.StartsWith("dcl"))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
foreach (Match cbMatch in SM5ConstantBufferUsageRegex.Matches(instruction))
|
||||
{
|
||||
var buffer = uint.Parse(cbMatch.Groups[1].Value);
|
||||
if (cbIndices.TryGetValue(buffer, out var i))
|
||||
{
|
||||
var swizzle = ParseVectorComponents(cbMatch.Groups[3].Value);
|
||||
if (int.TryParse(cbMatch.Groups[2].Value, out var vector))
|
||||
{
|
||||
if (vector < resourceBindings[i].Used.Length)
|
||||
{
|
||||
resourceBindings[i].Used[vector] |= swizzle;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
resourceBindings[i].UsedDynamically |= swizzle;
|
||||
}
|
||||
}
|
||||
}
|
||||
var tMatch = SM5TextureUsageRegex.Match(instruction);
|
||||
if (tMatch.Success)
|
||||
{
|
||||
var texture = uint.Parse(tMatch.Groups[2].Value);
|
||||
if (tIndices.TryGetValue(texture, out var i))
|
||||
{
|
||||
var outSwizzle = ParseVectorComponents(tMatch.Groups[1].Value);
|
||||
var rawInSwizzle = tMatch.Groups[3].Value;
|
||||
var inSwizzle = new StringBuilder(4);
|
||||
if ((outSwizzle & VectorComponents.X) != 0)
|
||||
{
|
||||
inSwizzle.Append(rawInSwizzle[0]);
|
||||
}
|
||||
if ((outSwizzle & VectorComponents.Y) != 0)
|
||||
{
|
||||
inSwizzle.Append(rawInSwizzle[1]);
|
||||
}
|
||||
if ((outSwizzle & VectorComponents.Z) != 0)
|
||||
{
|
||||
inSwizzle.Append(rawInSwizzle[2]);
|
||||
}
|
||||
if ((outSwizzle & VectorComponents.W) != 0)
|
||||
{
|
||||
inSwizzle.Append(rawInSwizzle[3]);
|
||||
}
|
||||
resourceBindings[i].Used[0] |= ParseVectorComponents(inSwizzle.ToString());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static VectorComponents ParseVectorComponents(string components)
|
||||
{
|
||||
components = components.ToUpperInvariant();
|
||||
return (components.Contains('X') ? VectorComponents.X : 0)
|
||||
| (components.Contains('Y') ? VectorComponents.Y : 0)
|
||||
| (components.Contains('Z') ? VectorComponents.Z : 0)
|
||||
| (components.Contains('W') ? VectorComponents.W : 0);
|
||||
}
|
||||
|
||||
private static Dictionary<string, string[]> PreParseHeader(ReadOnlySpan<string> header)
|
||||
{
|
||||
var sections = new Dictionary<string, string[]>();
|
||||
|
||||
void AddSection(string name, ReadOnlySpan<string> section)
|
||||
{
|
||||
while (section.Length > 0 && section[0].Length <= 3)
|
||||
{
|
||||
section = section[1..];
|
||||
}
|
||||
while (section.Length > 0 && section[^1].Length <= 3)
|
||||
{
|
||||
section = section[..^1];
|
||||
}
|
||||
sections.Add(name, Array.ConvertAll(section.ToArray(), ln => ln.Length <= 3 ? string.Empty : ln[3..]));
|
||||
}
|
||||
|
||||
var lastSectionName = "";
|
||||
var lastSectionStart = 0;
|
||||
for (var i = 1; i < header.Length - 1; ++i)
|
||||
{
|
||||
string current;
|
||||
if (header[i - 1].Length <= 3 && header[i + 1].Length <= 3 && (current = header[i].TrimEnd()).EndsWith(':'))
|
||||
{
|
||||
AddSection(lastSectionName, header[lastSectionStart..(i - 1)]);
|
||||
lastSectionName = current[3..^1];
|
||||
lastSectionStart = i + 2;
|
||||
++i; // The next line cannot match
|
||||
}
|
||||
}
|
||||
AddSection(lastSectionName, header[lastSectionStart..]);
|
||||
|
||||
return sections;
|
||||
}
|
||||
|
||||
private static (string[], string[][]) ParseTable(ReadOnlySpan<string> lines)
|
||||
{
|
||||
var columns = new List<Range>();
|
||||
{
|
||||
var dashLine = lines[1];
|
||||
for (var i = 0; true; /* this part intentionally left blank */)
|
||||
{
|
||||
var start = dashLine.IndexOf('-', i);
|
||||
if (start < 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
var end = dashLine.IndexOf(' ', start + 1);
|
||||
if (end < 0)
|
||||
{
|
||||
columns.Add(start..dashLine.Length);
|
||||
break;
|
||||
}
|
||||
else
|
||||
{
|
||||
columns.Add(start..end);
|
||||
i = end + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
var headers = new string[columns.Count];
|
||||
{
|
||||
var headerLine = lines[0];
|
||||
for (var i = 0; i < columns.Count; ++i)
|
||||
{
|
||||
headers[i] = headerLine[columns[i]].Trim();
|
||||
}
|
||||
}
|
||||
var data = new List<string[]>();
|
||||
foreach (var line in lines[2..])
|
||||
{
|
||||
var row = new string[columns.Count];
|
||||
for (var i = 0; i < columns.Count; ++i)
|
||||
{
|
||||
row[i] = line[columns[i]].Trim();
|
||||
}
|
||||
data.Add(row);
|
||||
}
|
||||
return (headers, data.ToArray());
|
||||
}
|
||||
}
|
||||
|
|
@ -77,7 +77,8 @@ public partial class MtrlFile
|
|||
foreach( var constant in ShaderPackage.Constants )
|
||||
{
|
||||
w.Write( constant.Id );
|
||||
w.Write( constant.Value );
|
||||
w.Write( constant.ByteOffset );
|
||||
w.Write( constant.ByteSize );
|
||||
}
|
||||
|
||||
foreach( var sampler in ShaderPackage.Samplers )
|
||||
|
|
|
|||
|
|
@ -239,7 +239,8 @@ public partial class MtrlFile : IWritable
|
|||
public struct Constant
|
||||
{
|
||||
public uint Id;
|
||||
public uint Value;
|
||||
public ushort ByteOffset;
|
||||
public ushort ByteSize;
|
||||
}
|
||||
|
||||
public struct ShaderPackageData
|
||||
|
|
@ -254,7 +255,20 @@ public partial class MtrlFile : IWritable
|
|||
|
||||
|
||||
public readonly uint Version;
|
||||
public bool Valid { get; }
|
||||
public bool Valid
|
||||
{
|
||||
get
|
||||
{
|
||||
foreach (var texture in Textures)
|
||||
{
|
||||
if (!texture.Path.Contains('/'))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public Texture[] Textures;
|
||||
public UvSet[] UvSets;
|
||||
|
|
@ -263,6 +277,8 @@ public partial class MtrlFile : IWritable
|
|||
public ShaderPackageData ShaderPackage;
|
||||
public byte[] AdditionalData;
|
||||
|
||||
public ShpkFile? AssociatedShpk;
|
||||
|
||||
public bool ApplyDyeTemplate(StmFile stm, int colorSetIdx, int rowIdx, StainId stainId)
|
||||
{
|
||||
if (colorSetIdx < 0 || colorSetIdx >= ColorDyeSets.Length || rowIdx is < 0 or >= ColorSet.RowArray.NumRows)
|
||||
|
|
@ -306,7 +322,41 @@ public partial class MtrlFile : IWritable
|
|||
return ret;
|
||||
}
|
||||
|
||||
public Span<float> GetConstantValues(Constant constant)
|
||||
{
|
||||
if ((constant.ByteOffset & 0x3) == 0 && (constant.ByteSize & 0x3) == 0
|
||||
&& ((constant.ByteOffset + constant.ByteSize) >> 2) <= ShaderPackage.ShaderValues.Length)
|
||||
{
|
||||
return ShaderPackage.ShaderValues.AsSpan().Slice(constant.ByteOffset >> 2, constant.ByteSize >> 2);
|
||||
}
|
||||
else
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public List<(Sampler?, ShpkFile.Resource?)> GetSamplersByTexture()
|
||||
{
|
||||
var samplers = new List<(Sampler?, ShpkFile.Resource?)>();
|
||||
for (var i = 0; i < Textures.Length; ++i)
|
||||
{
|
||||
samplers.Add((null, null));
|
||||
}
|
||||
foreach (var sampler in ShaderPackage.Samplers)
|
||||
{
|
||||
samplers[sampler.TextureIndex] = (sampler, AssociatedShpk?.GetSamplerById(sampler.SamplerId));
|
||||
}
|
||||
|
||||
return samplers;
|
||||
}
|
||||
|
||||
// Activator.CreateInstance can't use a ctor with a default value so this has to be made explicit
|
||||
public MtrlFile(byte[] data)
|
||||
: this(data, null)
|
||||
{
|
||||
}
|
||||
|
||||
public MtrlFile(byte[] data, Func<string, ShpkFile?>? loadAssociatedShpk = null)
|
||||
{
|
||||
using var stream = new MemoryStream(data);
|
||||
using var r = new BinaryReader(stream);
|
||||
|
|
@ -345,6 +395,8 @@ public partial class MtrlFile : IWritable
|
|||
|
||||
ShaderPackage.Name = UseOffset(strings, shaderPackageNameOffset);
|
||||
|
||||
AssociatedShpk = loadAssociatedShpk?.Invoke(ShaderPackage.Name);
|
||||
|
||||
AdditionalData = r.ReadBytes(additionalDataSize);
|
||||
for (var i = 0; i < ColorSets.Length; ++i)
|
||||
{
|
||||
|
|
@ -372,7 +424,6 @@ public partial class MtrlFile : IWritable
|
|||
ShaderPackage.Constants = r.ReadStructuresAsArray<Constant>(constantCount);
|
||||
ShaderPackage.Samplers = r.ReadStructuresAsArray<Sampler>(samplerCount);
|
||||
ShaderPackage.ShaderValues = r.ReadStructuresAsArray<float>(shaderValueListSize / 4);
|
||||
Valid = true;
|
||||
}
|
||||
|
||||
private static Texture[] ReadTextureOffsets(BinaryReader r, int count, out ushort[] offsets)
|
||||
|
|
|
|||
123
Penumbra.GameData/Files/ShpkFile.Write.cs
Normal file
123
Penumbra.GameData/Files/ShpkFile.Write.cs
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
|
||||
namespace Penumbra.GameData.Files;
|
||||
|
||||
public partial class ShpkFile
|
||||
{
|
||||
public byte[] Write()
|
||||
{
|
||||
using var stream = new MemoryStream();
|
||||
using var blobs = new MemoryStream();
|
||||
using (var w = new BinaryWriter(stream))
|
||||
{
|
||||
w.Write(ShPkMagic);
|
||||
w.Write(Unknown1);
|
||||
w.Write(DirectXVersion switch
|
||||
{
|
||||
DXVersion.DirectX9 => DX9Magic,
|
||||
DXVersion.DirectX11 => DX11Magic,
|
||||
_ => throw new NotImplementedException(),
|
||||
});
|
||||
long offsetsPosition = stream.Position;
|
||||
w.Write(0u); // Placeholder for file size
|
||||
w.Write(0u); // Placeholder for blobs offset
|
||||
w.Write(0u); // Placeholder for strings offset
|
||||
w.Write((uint)VertexShaders.Length);
|
||||
w.Write((uint)PixelShaders.Length);
|
||||
w.Write(MaterialParamsSize);
|
||||
w.Write((uint)MaterialParams.Length);
|
||||
w.Write((uint)Constants.Length);
|
||||
w.Write((uint)Samplers.Length);
|
||||
w.Write((uint)UnknownA.Length);
|
||||
w.Write((uint)UnknownB.Length);
|
||||
w.Write((uint)UnknownC.Length);
|
||||
w.Write(Unknown2);
|
||||
w.Write(Unknown3);
|
||||
w.Write(Unknown4);
|
||||
|
||||
WriteShaderArray(w, VertexShaders, blobs, Strings);
|
||||
WriteShaderArray(w, PixelShaders, blobs, Strings);
|
||||
|
||||
foreach (var materialParam in MaterialParams)
|
||||
{
|
||||
w.Write(materialParam.Id);
|
||||
w.Write(materialParam.ByteOffset);
|
||||
w.Write(materialParam.ByteSize);
|
||||
}
|
||||
|
||||
WriteResourceArray(w, Constants, Strings);
|
||||
WriteResourceArray(w, Samplers, Strings);
|
||||
|
||||
w.Write(Unknowns.Item1);
|
||||
w.Write(Unknowns.Item2);
|
||||
w.Write(Unknowns.Item3);
|
||||
|
||||
WriteUInt32PairArray(w, UnknownA);
|
||||
WriteUInt32PairArray(w, UnknownB);
|
||||
WriteUInt32PairArray(w, UnknownC);
|
||||
|
||||
w.Write(AdditionalData);
|
||||
|
||||
var blobsOffset = (int)stream.Position;
|
||||
blobs.WriteTo(stream);
|
||||
|
||||
var stringsOffset = (int)stream.Position;
|
||||
Strings.Data.WriteTo(stream);
|
||||
|
||||
var fileSize = (int)stream.Position;
|
||||
|
||||
stream.Seek(offsetsPosition, SeekOrigin.Begin);
|
||||
w.Write(fileSize);
|
||||
w.Write(blobsOffset);
|
||||
w.Write(stringsOffset);
|
||||
}
|
||||
|
||||
return stream.ToArray();
|
||||
}
|
||||
|
||||
private static void WriteResourceArray(BinaryWriter w, Resource[] array, StringPool strings)
|
||||
{
|
||||
foreach (var buf in array)
|
||||
{
|
||||
var (strOffset, strSize) = strings.FindOrAddString(buf.Name);
|
||||
w.Write(buf.Id);
|
||||
w.Write(strOffset);
|
||||
w.Write(strSize);
|
||||
w.Write(buf.Slot);
|
||||
w.Write(buf.Size);
|
||||
}
|
||||
}
|
||||
|
||||
private static void WriteShaderArray(BinaryWriter w, Shader[] array, MemoryStream blobs, StringPool strings)
|
||||
{
|
||||
foreach (var shader in array)
|
||||
{
|
||||
var blobOffset = (int)blobs.Position;
|
||||
blobs.Write(shader.AdditionalHeader);
|
||||
blobs.Write(shader.Blob);
|
||||
var blobSize = (int)blobs.Position - blobOffset;
|
||||
|
||||
w.Write(blobOffset);
|
||||
w.Write(blobSize);
|
||||
w.Write((ushort)shader.Constants.Length);
|
||||
w.Write((ushort)shader.Samplers.Length);
|
||||
w.Write((ushort)shader.UnknownX.Length);
|
||||
w.Write((ushort)shader.UnknownY.Length);
|
||||
|
||||
WriteResourceArray(w, shader.Constants, strings);
|
||||
WriteResourceArray(w, shader.Samplers, strings);
|
||||
WriteResourceArray(w, shader.UnknownX, strings);
|
||||
WriteResourceArray(w, shader.UnknownY, strings);
|
||||
}
|
||||
}
|
||||
|
||||
private static void WriteUInt32PairArray(BinaryWriter w, (uint, uint)[] array)
|
||||
{
|
||||
foreach (var (first, second) in array)
|
||||
{
|
||||
w.Write(first);
|
||||
w.Write(second);
|
||||
}
|
||||
}
|
||||
}
|
||||
644
Penumbra.GameData/Files/ShpkFile.cs
Normal file
644
Penumbra.GameData/Files/ShpkFile.cs
Normal file
|
|
@ -0,0 +1,644 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using Lumina.Extensions;
|
||||
using Lumina.Misc;
|
||||
using Penumbra.GameData.Data;
|
||||
|
||||
namespace Penumbra.GameData.Files;
|
||||
|
||||
public partial class ShpkFile : IWritable
|
||||
{
|
||||
public enum DXVersion : uint
|
||||
{
|
||||
DirectX9 = 9,
|
||||
DirectX11 = 11,
|
||||
}
|
||||
|
||||
public struct Resource
|
||||
{
|
||||
public uint Id;
|
||||
public string Name;
|
||||
public ushort Slot;
|
||||
public ushort Size;
|
||||
public DisassembledShader.VectorComponents[]? Used;
|
||||
public DisassembledShader.VectorComponents? UsedDynamically;
|
||||
}
|
||||
|
||||
public struct Shader
|
||||
{
|
||||
public DisassembledShader.ShaderStage Stage;
|
||||
public DXVersion DirectXVersion;
|
||||
public Resource[] Constants;
|
||||
public Resource[] Samplers;
|
||||
public Resource[] UnknownX;
|
||||
public Resource[] UnknownY;
|
||||
public byte[] AdditionalHeader;
|
||||
private byte[] _blob;
|
||||
private DisassembledShader? _disassembly;
|
||||
|
||||
public byte[] Blob
|
||||
{
|
||||
get => _blob;
|
||||
set
|
||||
{
|
||||
if (_blob == value)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (Stage != DisassembledShader.ShaderStage.Unspecified)
|
||||
{
|
||||
// Reject the blob entirely if we can't disassemble it or if we find inconsistencies.
|
||||
var disasm = DisassembledShader.Disassemble(value);
|
||||
if (disasm.Stage != Stage || (disasm.ShaderModel >> 8) + 6 != (uint)DirectXVersion)
|
||||
{
|
||||
throw new ArgumentException($"The supplied blob is a DirectX {(disasm.ShaderModel >> 8) + 6} {disasm.Stage} shader ; expected a DirectX {(uint)DirectXVersion} {Stage} shader.", nameof(value));
|
||||
}
|
||||
if (disasm.ShaderModel >= 0x0500)
|
||||
{
|
||||
var samplers = new Dictionary<uint, string>();
|
||||
var textures = new Dictionary<uint, string>();
|
||||
foreach (var binding in disasm.ResourceBindings)
|
||||
{
|
||||
switch (binding.Type)
|
||||
{
|
||||
case DisassembledShader.ResourceType.Texture:
|
||||
textures[binding.Slot] = NormalizeResourceName(binding.Name);
|
||||
break;
|
||||
case DisassembledShader.ResourceType.Sampler:
|
||||
samplers[binding.Slot] = NormalizeResourceName(binding.Name);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (samplers.Count != textures.Count || !samplers.All(pair => textures.TryGetValue(pair.Key, out var texName) && pair.Value == texName))
|
||||
{
|
||||
throw new ArgumentException($"The supplied blob has inconsistent shader and texture allocation.");
|
||||
}
|
||||
}
|
||||
_blob = value;
|
||||
_disassembly = disasm;
|
||||
}
|
||||
else
|
||||
{
|
||||
_blob = value;
|
||||
_disassembly = null;
|
||||
}
|
||||
UpdateUsed();
|
||||
}
|
||||
}
|
||||
|
||||
public DisassembledShader? Disassembly => _disassembly;
|
||||
|
||||
public Resource? GetConstantById(uint id)
|
||||
{
|
||||
return Constants.Select(res => new Resource?(res)).FirstOrDefault(res => res!.Value.Id == id);
|
||||
}
|
||||
|
||||
public Resource? GetConstantByName(string name)
|
||||
{
|
||||
return Constants.Select(res => new Resource?(res)).FirstOrDefault(res => res!.Value.Name == name);
|
||||
}
|
||||
|
||||
public Resource? GetSamplerById(uint id)
|
||||
{
|
||||
return Samplers.Select(res => new Resource?(res)).FirstOrDefault(res => res!.Value.Id == id);
|
||||
}
|
||||
|
||||
public Resource? GetSamplerByName(string name)
|
||||
{
|
||||
return Samplers.Select(res => new Resource?(res)).FirstOrDefault(res => res!.Value.Name == name);
|
||||
}
|
||||
|
||||
public void UpdateResources(ShpkFile file)
|
||||
{
|
||||
if (_disassembly == null)
|
||||
{
|
||||
throw new InvalidOperationException();
|
||||
}
|
||||
var constants = new List<Resource>();
|
||||
var samplers = new List<Resource>();
|
||||
foreach (var binding in _disassembly.ResourceBindings)
|
||||
{
|
||||
switch (binding.Type)
|
||||
{
|
||||
case DisassembledShader.ResourceType.ConstantBuffer:
|
||||
var name = NormalizeResourceName(binding.Name);
|
||||
// We want to preserve IDs as much as possible, and to deterministically generate new ones, to maximize compatibility.
|
||||
var id = GetConstantByName(name)?.Id ?? file.GetConstantByName(name)?.Id ?? Crc32.Get(name);
|
||||
constants.Add(new Resource
|
||||
{
|
||||
Id = id,
|
||||
Name = name,
|
||||
Slot = (ushort)binding.Slot,
|
||||
Size = (ushort)binding.RegisterCount,
|
||||
Used = binding.Used,
|
||||
});
|
||||
break;
|
||||
case DisassembledShader.ResourceType.Texture:
|
||||
name = NormalizeResourceName(binding.Name);
|
||||
id = GetSamplerByName(name)?.Id ?? file.GetSamplerByName(name)?.Id ?? Crc32.Get(name);
|
||||
samplers.Add(new Resource
|
||||
{
|
||||
Id = id,
|
||||
Name = name,
|
||||
Slot = (ushort)binding.Slot,
|
||||
Size = (ushort)binding.Slot,
|
||||
Used = binding.Used,
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
Constants = constants.ToArray();
|
||||
Samplers = samplers.ToArray();
|
||||
}
|
||||
|
||||
private void UpdateUsed()
|
||||
{
|
||||
if (_disassembly != null)
|
||||
{
|
||||
var cbUsage = new Dictionary<string, (DisassembledShader.VectorComponents[], DisassembledShader.VectorComponents)>();
|
||||
var tUsage = new Dictionary<string, (DisassembledShader.VectorComponents[], DisassembledShader.VectorComponents)>();
|
||||
foreach (var binding in _disassembly.ResourceBindings)
|
||||
{
|
||||
switch (binding.Type)
|
||||
{
|
||||
case DisassembledShader.ResourceType.ConstantBuffer:
|
||||
cbUsage[NormalizeResourceName(binding.Name)] = (binding.Used, binding.UsedDynamically);
|
||||
break;
|
||||
case DisassembledShader.ResourceType.Texture:
|
||||
tUsage[NormalizeResourceName(binding.Name)] = (binding.Used, binding.UsedDynamically);
|
||||
break;
|
||||
}
|
||||
}
|
||||
for (var i = 0; i < Constants.Length; ++i)
|
||||
{
|
||||
if (cbUsage.TryGetValue(Constants[i].Name, out var usage))
|
||||
{
|
||||
Constants[i].Used = usage.Item1;
|
||||
Constants[i].UsedDynamically = usage.Item2;
|
||||
}
|
||||
else
|
||||
{
|
||||
Constants[i].Used = null;
|
||||
Constants[i].UsedDynamically = null;
|
||||
}
|
||||
}
|
||||
for (var i = 0; i < Samplers.Length; ++i)
|
||||
{
|
||||
if (tUsage.TryGetValue(Samplers[i].Name, out var usage))
|
||||
{
|
||||
Samplers[i].Used = usage.Item1;
|
||||
Samplers[i].UsedDynamically = usage.Item2;
|
||||
}
|
||||
else
|
||||
{
|
||||
Samplers[i].Used = null;
|
||||
Samplers[i].UsedDynamically = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
ClearUsed(Constants);
|
||||
ClearUsed(Samplers);
|
||||
}
|
||||
}
|
||||
|
||||
private static string NormalizeResourceName(string resourceName)
|
||||
{
|
||||
var dot = resourceName.IndexOf('.');
|
||||
if (dot >= 0)
|
||||
{
|
||||
return resourceName[..dot];
|
||||
}
|
||||
else if (resourceName.EndsWith("_S") || resourceName.EndsWith("_T"))
|
||||
{
|
||||
return resourceName[..^2];
|
||||
}
|
||||
else
|
||||
{
|
||||
return resourceName;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public struct MaterialParam
|
||||
{
|
||||
public uint Id;
|
||||
public ushort ByteOffset;
|
||||
public ushort ByteSize;
|
||||
}
|
||||
|
||||
public class StringPool
|
||||
{
|
||||
public MemoryStream Data;
|
||||
public List<int> StartingOffsets;
|
||||
|
||||
public StringPool(ReadOnlySpan<byte> bytes)
|
||||
{
|
||||
Data = new MemoryStream();
|
||||
Data.Write(bytes);
|
||||
StartingOffsets = new List<int>
|
||||
{
|
||||
0,
|
||||
};
|
||||
for (var i = 0; i < bytes.Length; ++i)
|
||||
{
|
||||
if (bytes[i] == 0)
|
||||
{
|
||||
StartingOffsets.Add(i + 1);
|
||||
}
|
||||
}
|
||||
if (StartingOffsets[^1] == bytes.Length)
|
||||
{
|
||||
StartingOffsets.RemoveAt(StartingOffsets.Count - 1);
|
||||
}
|
||||
else
|
||||
{
|
||||
Data.WriteByte(0);
|
||||
}
|
||||
}
|
||||
|
||||
public string GetString(int offset, int size)
|
||||
{
|
||||
return Encoding.UTF8.GetString(Data.GetBuffer().AsSpan().Slice(offset, size));
|
||||
}
|
||||
|
||||
public string GetNullTerminatedString(int offset)
|
||||
{
|
||||
var str = Data.GetBuffer().AsSpan()[offset..];
|
||||
var size = str.IndexOf((byte)0);
|
||||
if (size >= 0)
|
||||
{
|
||||
str = str[..size];
|
||||
}
|
||||
return Encoding.UTF8.GetString(str);
|
||||
}
|
||||
|
||||
public (int, int) FindOrAddString(string str)
|
||||
{
|
||||
var dataSpan = Data.GetBuffer().AsSpan();
|
||||
var bytes = Encoding.UTF8.GetBytes(str);
|
||||
foreach (var offset in StartingOffsets)
|
||||
{
|
||||
if (offset + bytes.Length > Data.Length)
|
||||
{
|
||||
break;
|
||||
}
|
||||
var strSpan = dataSpan[offset..];
|
||||
var match = true;
|
||||
for (var i = 0; i < bytes.Length; ++i)
|
||||
{
|
||||
if (strSpan[i] != bytes[i])
|
||||
{
|
||||
match = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (match && strSpan[bytes.Length] == 0)
|
||||
{
|
||||
return (offset, bytes.Length);
|
||||
}
|
||||
}
|
||||
Data.Seek(0L, SeekOrigin.End);
|
||||
var newOffset = (int)Data.Position;
|
||||
StartingOffsets.Add(newOffset);
|
||||
Data.Write(bytes);
|
||||
Data.WriteByte(0);
|
||||
return (newOffset, bytes.Length);
|
||||
}
|
||||
}
|
||||
|
||||
private const uint ShPkMagic = 0x6B506853u; // bytes of ShPk
|
||||
private const uint DX9Magic = 0x00395844u; // bytes of DX9\0
|
||||
private const uint DX11Magic = 0x31315844u; // bytes of DX11
|
||||
|
||||
public const uint MaterialParamsConstantId = 0x64D12851u;
|
||||
|
||||
public uint Unknown1;
|
||||
public DXVersion DirectXVersion;
|
||||
public Shader[] VertexShaders;
|
||||
public Shader[] PixelShaders;
|
||||
public uint MaterialParamsSize;
|
||||
public MaterialParam[] MaterialParams;
|
||||
public Resource[] Constants;
|
||||
public Resource[] Samplers;
|
||||
public (uint, uint)[] UnknownA;
|
||||
public (uint, uint)[] UnknownB;
|
||||
public (uint, uint)[] UnknownC;
|
||||
public uint Unknown2;
|
||||
public uint Unknown3;
|
||||
public uint Unknown4;
|
||||
public (uint, uint, uint) Unknowns;
|
||||
public byte[] AdditionalData;
|
||||
public StringPool Strings; // Cannot be safely discarded yet, we don't know if AdditionalData references it
|
||||
|
||||
public bool Valid { get; private set; }
|
||||
private bool _changed;
|
||||
|
||||
public MaterialParam? GetMaterialParamById(uint id)
|
||||
{
|
||||
return MaterialParams.Select(param => new MaterialParam?(param)).FirstOrDefault(param => param!.Value.Id == id);
|
||||
}
|
||||
|
||||
public Resource? GetConstantById(uint id)
|
||||
{
|
||||
return Constants.Select(res => new Resource?(res)).FirstOrDefault(res => res!.Value.Id == id);
|
||||
}
|
||||
|
||||
public Resource? GetConstantByName(string name)
|
||||
{
|
||||
return Constants.Select(res => new Resource?(res)).FirstOrDefault(res => res!.Value.Name == name);
|
||||
}
|
||||
|
||||
public Resource? GetSamplerById(uint id)
|
||||
{
|
||||
return Samplers.Select(res => new Resource?(res)).FirstOrDefault(res => res!.Value.Id == id);
|
||||
}
|
||||
|
||||
public Resource? GetSamplerByName(string name)
|
||||
{
|
||||
return Samplers.Select(res => new Resource?(res)).FirstOrDefault(res => res!.Value.Name == name);
|
||||
}
|
||||
|
||||
// Activator.CreateInstance can't use a ctor with a default value so this has to be made explicit
|
||||
public ShpkFile(byte[] data)
|
||||
: this(data, false)
|
||||
{
|
||||
}
|
||||
|
||||
public ShpkFile(byte[] data, bool disassemble = false)
|
||||
{
|
||||
using var stream = new MemoryStream(data);
|
||||
using var r = new BinaryReader(stream);
|
||||
|
||||
if (r.ReadUInt32() != ShPkMagic)
|
||||
{
|
||||
throw new InvalidDataException();
|
||||
}
|
||||
Unknown1 = r.ReadUInt32();
|
||||
DirectXVersion = r.ReadUInt32() switch
|
||||
{
|
||||
DX9Magic => DXVersion.DirectX9,
|
||||
DX11Magic => DXVersion.DirectX11,
|
||||
_ => throw new InvalidDataException(),
|
||||
};
|
||||
if (r.ReadUInt32() != data.Length)
|
||||
{
|
||||
throw new InvalidDataException();
|
||||
}
|
||||
var blobsOffset = r.ReadUInt32();
|
||||
var stringsOffset = r.ReadUInt32();
|
||||
var vertexShaderCount = r.ReadUInt32();
|
||||
var pixelShaderCount = r.ReadUInt32();
|
||||
MaterialParamsSize = r.ReadUInt32();
|
||||
var materialParamCount = r.ReadUInt32();
|
||||
var constantCount = r.ReadUInt32();
|
||||
var samplerCount = r.ReadUInt32();
|
||||
var unknownACount = r.ReadUInt32();
|
||||
var unknownBCount = r.ReadUInt32();
|
||||
var unknownCCount = r.ReadUInt32();
|
||||
Unknown2 = r.ReadUInt32();
|
||||
Unknown3 = r.ReadUInt32();
|
||||
Unknown4 = r.ReadUInt32();
|
||||
|
||||
var blobs = new ReadOnlySpan<byte>(data, (int)blobsOffset, (int)(stringsOffset - blobsOffset));
|
||||
Strings = new StringPool(new ReadOnlySpan<byte>(data, (int)stringsOffset, (int)(data.Length - stringsOffset)));
|
||||
|
||||
VertexShaders = ReadShaderArray(r, (int)vertexShaderCount, DisassembledShader.ShaderStage.Vertex, DirectXVersion, disassemble, blobs, Strings);
|
||||
PixelShaders = ReadShaderArray(r, (int)pixelShaderCount, DisassembledShader.ShaderStage.Pixel, DirectXVersion, disassemble, blobs, Strings);
|
||||
|
||||
MaterialParams = r.ReadStructuresAsArray<MaterialParam>((int)materialParamCount);
|
||||
|
||||
Constants = ReadResourceArray(r, (int)constantCount, Strings);
|
||||
Samplers = ReadResourceArray(r, (int)samplerCount, Strings);
|
||||
|
||||
var unk1 = r.ReadUInt32();
|
||||
var unk2 = r.ReadUInt32();
|
||||
var unk3 = r.ReadUInt32();
|
||||
Unknowns = (unk1, unk2, unk3);
|
||||
|
||||
UnknownA = ReadUInt32PairArray(r, (int)unknownACount);
|
||||
UnknownB = ReadUInt32PairArray(r, (int)unknownBCount);
|
||||
UnknownC = ReadUInt32PairArray(r, (int)unknownCCount);
|
||||
|
||||
AdditionalData = r.ReadBytes((int)(blobsOffset - r.BaseStream.Position));
|
||||
|
||||
if (disassemble)
|
||||
{
|
||||
UpdateUsed();
|
||||
}
|
||||
|
||||
Valid = true;
|
||||
_changed = false;
|
||||
}
|
||||
|
||||
public void UpdateResources()
|
||||
{
|
||||
var constants = new Dictionary<uint, Resource>();
|
||||
var samplers = new Dictionary<uint, Resource>();
|
||||
static void CollectResources(Dictionary<uint, Resource> resources, Resource[] shaderResources, Func<uint, Resource?> getExistingById, bool isSamplers)
|
||||
{
|
||||
foreach (var resource in shaderResources)
|
||||
{
|
||||
if (resources.TryGetValue(resource.Id, out var carry) && isSamplers)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
var existing = getExistingById(resource.Id);
|
||||
resources[resource.Id] = new Resource
|
||||
{
|
||||
Id = resource.Id,
|
||||
Name = resource.Name,
|
||||
Slot = existing?.Slot ?? (isSamplers ? (ushort)2 : (ushort)65535),
|
||||
Size = isSamplers ? (existing?.Size ?? 0) : Math.Max(carry.Size, resource.Size),
|
||||
Used = null,
|
||||
UsedDynamically = null,
|
||||
};
|
||||
}
|
||||
}
|
||||
foreach (var shader in VertexShaders)
|
||||
{
|
||||
CollectResources(constants, shader.Constants, GetConstantById, false);
|
||||
CollectResources(samplers, shader.Samplers, GetSamplerById, true);
|
||||
}
|
||||
foreach (var shader in PixelShaders)
|
||||
{
|
||||
CollectResources(constants, shader.Constants, GetConstantById, false);
|
||||
CollectResources(samplers, shader.Samplers, GetSamplerById, true);
|
||||
}
|
||||
Constants = constants.Values.ToArray();
|
||||
Samplers = samplers.Values.ToArray();
|
||||
UpdateUsed();
|
||||
MaterialParamsSize = (GetConstantById(MaterialParamsConstantId)?.Size ?? 0u) << 4;
|
||||
foreach (var param in MaterialParams)
|
||||
{
|
||||
MaterialParamsSize = Math.Max(MaterialParamsSize, (uint)param.ByteOffset + param.ByteSize);
|
||||
}
|
||||
MaterialParamsSize = (MaterialParamsSize + 0xFu) & ~0xFu;
|
||||
}
|
||||
|
||||
private void UpdateUsed()
|
||||
{
|
||||
var cUsage = new Dictionary<uint, (DisassembledShader.VectorComponents[], DisassembledShader.VectorComponents)>();
|
||||
var sUsage = new Dictionary<uint, (DisassembledShader.VectorComponents[], DisassembledShader.VectorComponents)>();
|
||||
static void CollectUsage(Dictionary<uint, (DisassembledShader.VectorComponents[], DisassembledShader.VectorComponents)> usage, Resource[] resources)
|
||||
{
|
||||
foreach (var resource in resources)
|
||||
{
|
||||
if (resource.Used == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
usage.TryGetValue(resource.Id, out var carry);
|
||||
carry.Item1 ??= Array.Empty<DisassembledShader.VectorComponents>();
|
||||
var combined = new DisassembledShader.VectorComponents[Math.Max(carry.Item1.Length, resource.Used.Length)];
|
||||
for (var i = 0; i < combined.Length; ++i)
|
||||
{
|
||||
combined[i] = (i < carry.Item1.Length ? carry.Item1[i] : 0) | (i < resource.Used.Length ? resource.Used[i] : 0);
|
||||
}
|
||||
usage[resource.Id] = (combined, carry.Item2 | (resource.UsedDynamically ?? 0));
|
||||
}
|
||||
}
|
||||
foreach (var shader in VertexShaders)
|
||||
{
|
||||
CollectUsage(cUsage, shader.Constants);
|
||||
CollectUsage(sUsage, shader.Samplers);
|
||||
}
|
||||
foreach (var shader in PixelShaders)
|
||||
{
|
||||
CollectUsage(cUsage, shader.Constants);
|
||||
CollectUsage(sUsage, shader.Samplers);
|
||||
}
|
||||
for (var i = 0; i < Constants.Length; ++i)
|
||||
{
|
||||
if (cUsage.TryGetValue(Constants[i].Id, out var usage))
|
||||
{
|
||||
Constants[i].Used = usage.Item1;
|
||||
Constants[i].UsedDynamically = usage.Item2;
|
||||
}
|
||||
else
|
||||
{
|
||||
Constants[i].Used = null;
|
||||
Constants[i].UsedDynamically = null;
|
||||
}
|
||||
}
|
||||
for (var i = 0; i < Samplers.Length; ++i)
|
||||
{
|
||||
if (sUsage.TryGetValue(Samplers[i].Id, out var usage))
|
||||
{
|
||||
Samplers[i].Used = usage.Item1;
|
||||
Samplers[i].UsedDynamically = usage.Item2;
|
||||
}
|
||||
else
|
||||
{
|
||||
Samplers[i].Used = null;
|
||||
Samplers[i].UsedDynamically = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void SetInvalid()
|
||||
{
|
||||
Valid = false;
|
||||
}
|
||||
|
||||
public void SetChanged()
|
||||
{
|
||||
_changed = true;
|
||||
}
|
||||
|
||||
public bool IsChanged()
|
||||
{
|
||||
var changed = _changed;
|
||||
_changed = false;
|
||||
return changed;
|
||||
}
|
||||
|
||||
private static void ClearUsed(Resource[] resources)
|
||||
{
|
||||
for (var i = 0; i < resources.Length; ++i)
|
||||
{
|
||||
resources[i].Used = null;
|
||||
resources[i].UsedDynamically = null;
|
||||
}
|
||||
}
|
||||
|
||||
private static Resource[] ReadResourceArray(BinaryReader r, int count, StringPool strings)
|
||||
{
|
||||
var ret = new Resource[count];
|
||||
for (var i = 0; i < count; ++i)
|
||||
{
|
||||
var buf = new Resource();
|
||||
|
||||
buf.Id = r.ReadUInt32();
|
||||
var strOffset = r.ReadUInt32();
|
||||
var strSize = r.ReadUInt32();
|
||||
buf.Name = strings.GetString((int)strOffset, (int)strSize);
|
||||
buf.Slot = r.ReadUInt16();
|
||||
buf.Size = r.ReadUInt16();
|
||||
|
||||
ret[i] = buf;
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
private static Shader[] ReadShaderArray(BinaryReader r, int count, DisassembledShader.ShaderStage stage, DXVersion directX, bool disassemble, ReadOnlySpan<byte> blobs, StringPool strings)
|
||||
{
|
||||
var extraHeaderSize = stage switch
|
||||
{
|
||||
DisassembledShader.ShaderStage.Vertex => directX switch
|
||||
{
|
||||
DXVersion.DirectX9 => 4,
|
||||
DXVersion.DirectX11 => 8,
|
||||
_ => throw new NotImplementedException(),
|
||||
},
|
||||
_ => 0,
|
||||
};
|
||||
|
||||
var ret = new Shader[count];
|
||||
for (var i = 0; i < count; ++i)
|
||||
{
|
||||
var blobOffset = r.ReadUInt32();
|
||||
var blobSize = r.ReadUInt32();
|
||||
var constantCount = r.ReadUInt16();
|
||||
var samplerCount = r.ReadUInt16();
|
||||
var unknownXCount = r.ReadUInt16();
|
||||
var unknownYCount = r.ReadUInt16();
|
||||
|
||||
var rawBlob = blobs.Slice((int)blobOffset, (int)blobSize);
|
||||
|
||||
var shader = new Shader();
|
||||
|
||||
shader.Stage = disassemble ? stage : DisassembledShader.ShaderStage.Unspecified;
|
||||
shader.DirectXVersion = directX;
|
||||
shader.Constants = ReadResourceArray(r, constantCount, strings);
|
||||
shader.Samplers = ReadResourceArray(r, samplerCount, strings);
|
||||
shader.UnknownX = ReadResourceArray(r, unknownXCount, strings);
|
||||
shader.UnknownY = ReadResourceArray(r, unknownYCount, strings);
|
||||
shader.AdditionalHeader = rawBlob[..extraHeaderSize].ToArray();
|
||||
shader.Blob = rawBlob[extraHeaderSize..].ToArray();
|
||||
|
||||
ret[i] = shader;
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
private static (uint, uint)[] ReadUInt32PairArray(BinaryReader r, int count)
|
||||
{
|
||||
var ret = new (uint, uint)[count];
|
||||
for (var i = 0; i < count; ++i)
|
||||
{
|
||||
var first = r.ReadUInt32();
|
||||
var second = r.ReadUInt32();
|
||||
|
||||
ret[i] = (first, second);
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
65
Penumbra.GameData/Interop/D3DCompiler.cs
Normal file
65
Penumbra.GameData/Interop/D3DCompiler.cs
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
|
||||
namespace Penumbra.GameData.Interop;
|
||||
|
||||
internal static class D3DCompiler
|
||||
{
|
||||
[Guid("8BA5FB08-5195-40e2-AC58-0D989C3A0102")]
|
||||
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
||||
private interface ID3DBlob
|
||||
{
|
||||
[PreserveSig]
|
||||
public unsafe void* GetBufferPointer();
|
||||
|
||||
[PreserveSig]
|
||||
public UIntPtr GetBufferSize();
|
||||
}
|
||||
|
||||
[Flags]
|
||||
public enum DisassembleFlags : uint
|
||||
{
|
||||
EnableColorCode = 1,
|
||||
EnableDefaultValuePrints = 2,
|
||||
EnableInstructionNumbering = 4,
|
||||
EnableInstructionCycle = 8,
|
||||
DisableDebugInfo = 16,
|
||||
EnableInstructionOffset = 32,
|
||||
InstructionOnly = 64,
|
||||
PrintHexLiterals = 128,
|
||||
}
|
||||
|
||||
public static unsafe string Disassemble(ReadOnlySpan<byte> blob, DisassembleFlags flags = 0, string comments = "")
|
||||
{
|
||||
ID3DBlob? disassembly;
|
||||
int hr;
|
||||
fixed (byte* pSrcData = blob)
|
||||
{
|
||||
hr = D3DDisassemble(pSrcData, new UIntPtr((uint)blob.Length), (uint)flags, comments, out disassembly);
|
||||
}
|
||||
Marshal.ThrowExceptionForHR(hr);
|
||||
var ret = Encoding.UTF8.GetString(BlobContents(disassembly));
|
||||
GC.KeepAlive(disassembly);
|
||||
return ret;
|
||||
}
|
||||
|
||||
private static unsafe ReadOnlySpan<byte> BlobContents(ID3DBlob? blob)
|
||||
{
|
||||
if (blob == null)
|
||||
{
|
||||
return ReadOnlySpan<byte>.Empty;
|
||||
}
|
||||
|
||||
return new ReadOnlySpan<byte>(blob.GetBufferPointer(), (int)blob.GetBufferSize().ToUInt32());
|
||||
}
|
||||
|
||||
[PreserveSig]
|
||||
[DllImport("D3DCompiler_47.dll")]
|
||||
private extern static unsafe int D3DDisassemble(
|
||||
[In] byte* pSrcData,
|
||||
[In] UIntPtr srcDataSize,
|
||||
uint flags,
|
||||
[MarshalAs(UnmanagedType.LPStr)] string szComments,
|
||||
out ID3DBlob? ppDisassembly);
|
||||
}
|
||||
|
|
@ -76,6 +76,7 @@ public partial class Mod
|
|||
private List< FileRegistry > _mtrlFiles = null!;
|
||||
private List< FileRegistry > _mdlFiles = null!;
|
||||
private List< FileRegistry > _texFiles = null!;
|
||||
private List< FileRegistry > _shpkFiles = null!;
|
||||
private readonly HashSet< Utf8GamePath > _usedPaths = new();
|
||||
|
||||
// All paths that are used in
|
||||
|
|
@ -93,6 +94,9 @@ public partial class Mod
|
|||
public IReadOnlyList< FileRegistry > TexFiles
|
||||
=> _texFiles;
|
||||
|
||||
public IReadOnlyList< FileRegistry > ShpkFiles
|
||||
=> _shpkFiles;
|
||||
|
||||
// Remove all path redirections where the pointed-to file does not exist.
|
||||
public void RemoveMissingPaths()
|
||||
{
|
||||
|
|
@ -140,6 +144,7 @@ public partial class Mod
|
|||
_mtrlFiles = _availableFiles.Where( f => f.File.FullName.EndsWith( ".mtrl", StringComparison.OrdinalIgnoreCase ) ).ToList();
|
||||
_mdlFiles = _availableFiles.Where( f => f.File.FullName.EndsWith( ".mdl", StringComparison.OrdinalIgnoreCase ) ).ToList();
|
||||
_texFiles = _availableFiles.Where( f => f.File.FullName.EndsWith( ".tex", StringComparison.OrdinalIgnoreCase ) ).ToList();
|
||||
_shpkFiles = _availableFiles.Where( f => f.File.FullName.EndsWith( ".shpk", StringComparison.OrdinalIgnoreCase ) ).ToList();
|
||||
FileChanges = false;
|
||||
foreach( var subMod in _mod.AllSubMods )
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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 )
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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,23 +169,349 @@ 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 ) ) )
|
||||
{
|
||||
foreach( var tex in file.Textures )
|
||||
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" ) )
|
||||
{
|
||||
ImRaii.TreeNode( $"{tex.Path} - {tex.Flags:X4}", ImGuiTreeNodeFlags.Leaf ).Dispose();
|
||||
_materialFileDialog.OpenFileDialog( $"Associate modded ShPk file...", ".shpk", ( success, name ) =>
|
||||
{
|
||||
if( !success )
|
||||
{
|
||||
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 ) )
|
||||
{
|
||||
|
|
@ -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}";
|
||||
}
|
||||
}
|
||||
557
Penumbra/UI/Classes/ModEditWindow.ShaderPackages.cs
Normal file
557
Penumbra/UI/Classes/ModEditWindow.ShaderPackages.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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 );
|
||||
}
|
||||
|
||||
|
|
|
|||
110
Penumbra/Util/IndexSet.cs
Normal file
110
Penumbra/Util/IndexSet.cs
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Penumbra.Util;
|
||||
|
||||
public class IndexSet : IEnumerable<int>
|
||||
{
|
||||
private readonly BitArray _set;
|
||||
private int _count;
|
||||
|
||||
public int Capacity => _set.Count;
|
||||
|
||||
public int Count => _count;
|
||||
|
||||
public bool this[Index index]
|
||||
{
|
||||
get => _set[index];
|
||||
set
|
||||
{
|
||||
if( value )
|
||||
{
|
||||
Add( index );
|
||||
}
|
||||
else
|
||||
{
|
||||
Remove( index );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public IndexSet( int capacity, bool initiallyFull )
|
||||
{
|
||||
_set = new BitArray( capacity, initiallyFull );
|
||||
_count = initiallyFull ? capacity : 0;
|
||||
}
|
||||
|
||||
public bool Add( Index index )
|
||||
{
|
||||
var ret = !_set[index];
|
||||
if( ret )
|
||||
{
|
||||
++_count;
|
||||
_set[index] = true;
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
public bool Remove( Index index )
|
||||
{
|
||||
var ret = _set[index];
|
||||
if( ret )
|
||||
{
|
||||
--_count;
|
||||
_set[index] = false;
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
public int AddRange( int offset, int length )
|
||||
{
|
||||
var ret = 0;
|
||||
for( var idx = 0; idx < length; ++idx )
|
||||
{
|
||||
if( Add( offset + idx ) )
|
||||
{
|
||||
++ret;
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
public int RemoveRange( int offset, int length )
|
||||
{
|
||||
var ret = 0;
|
||||
for( var idx = 0; idx < length; ++idx )
|
||||
{
|
||||
if( Remove( offset + idx ) )
|
||||
{
|
||||
++ret;
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
public IEnumerator<int> GetEnumerator()
|
||||
{
|
||||
if( _count > 0 )
|
||||
{
|
||||
var capacity = _set.Count;
|
||||
var remaining = _count;
|
||||
for( var i = 0; i < capacity; ++i )
|
||||
{
|
||||
if( _set[i] )
|
||||
{
|
||||
yield return i;
|
||||
if( --remaining == 0 )
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator()
|
||||
{
|
||||
return GetEnumerator();
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue