Mtrl shader resource editing, ShPk editing

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

View file

@ -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());
}
}

View file

@ -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 )

View file

@ -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)

View 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);
}
}
}

View 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;
}
}

View 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);
}