diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml new file mode 100644 index 00000000..17c67885 --- /dev/null +++ b/.github/workflows/build.yaml @@ -0,0 +1,39 @@ +name: build + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + workflow_dispatch: + +jobs: + build: + + runs-on: windows-latest + defaults: + run: + working-directory: ./src + steps: + - uses: actions/checkout@v2 + - name: Setup .NET + uses: actions/setup-dotnet@v1 + with: + dotnet-version: 5.0.x + - name: Restore previous dependencies + uses: actions/cache@v2 + with: + path: ~/.nuget/packages + key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj') }} + - name: Restore dependencies + run: dotnet restore DXDecompiler.sln + - name: Build + id: build + run: dotnet build --no-restore + - name: Publish + id: publish + run: dotnet publish DXDecompilerCmd --no-build + - name: Upload published files + uses: actions/upload-artifact@v2.3.1 + with: + path: src/DXDecompilerCmd/bin/**/publish diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml new file mode 100644 index 00000000..7f0fb22f --- /dev/null +++ b/.github/workflows/test.yaml @@ -0,0 +1,33 @@ +name: test + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + workflow_dispatch: + +jobs: + test: + runs-on: windows-latest + defaults: + run: + working-directory: ./src + steps: + - uses: actions/checkout@v2 + - name: Setup .NET + uses: actions/setup-dotnet@v1 + with: + dotnet-version: 5.0.x + - name: Restore previous dependencies + uses: actions/cache@v2 + with: + path: ~/.nuget/packages + key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj') }} + - name: Restore dependencies + run: dotnet restore DXDecompiler.sln + - name: Build + id: build + run: dotnet build --no-restore + - name: Test + run: dotnet test DXDecompiler.Tests --no-build --verbosity normal diff --git a/README.md b/README.md index ebfc1d60..a3453d34 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,14 @@ DXDecompiler ========== +[![build](https://github.com/lanyizi/DXDecompiler/actions/workflows/build.yaml/badge.svg)](https://github.com/lanyizi/DXDecompiler/actions/workflows/build.yaml) +[![test](https://github.com/lanyizi/DXDecompiler/actions/workflows/test.yaml/badge.svg)](https://github.com/lanyizi/DXDecompiler/actions/workflows/test.yaml) + DXDecompiler is a Direct3D shader bytecode decompiler for .NET. It is in currently in very early development. +Latest CI build artifact can be downloaded [here](https://nightly.link/lanyizi/DXDecompiler/workflows/build.yaml/master/artifact.zip). +Requires [.NET 5 Runtime](https://dotnet.microsoft.com/en-us/download/dotnet/5.0/runtime). + Acknowledgements ---------------- DXDecompiler uses code and research from the following projects. diff --git a/src/DXDecompiler.Tests/DXDecompiler.Tests.csproj b/src/DXDecompiler.Tests/DXDecompiler.Tests.csproj index 5e255023..0cf563aa 100644 --- a/src/DXDecompiler.Tests/DXDecompiler.Tests.csproj +++ b/src/DXDecompiler.Tests/DXDecompiler.Tests.csproj @@ -1,87 +1,36 @@ - - - - - + - Debug - AnyCPU - {3D19DAC1-E727-43BA-A264-A64408B4D598} - Library - Properties - DXDecompiler.Tests - DXDecompiler.Tests - v4.7.2 - 512 - + net5.0 ..\ - true - - + SlimShader.Tests + SlimShader.Tests + Copyright © 2012 + bin\$(Configuration)\ - true full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 x86 true - false pdbonly - true - bin\Release\ - TRACE - prompt - 4 - false + - - ..\packages\NUnit.3.12.0\lib\net40\nunit.framework.dll - - - ..\packages\SharpDX.4.2.0\lib\net40\SharpDX.dll - - - ..\packages\SharpDX.D3DCompiler.4.2.0\lib\net40\SharpDX.D3DCompiler.dll - - - ..\packages\SharpDX.Direct3D10.4.2.0\lib\net40\SharpDX.Direct3D10.dll - - - ..\packages\SharpDX.Direct3D11.4.2.0\lib\net40\SharpDX.Direct3D11.dll - - - ..\packages\SharpDX.Direct3D11.Effects.4.2.0\lib\net40\SharpDX.Direct3D11.Effects.dll - - - ..\packages\SharpDX.Direct3D9.4.2.0\lib\net40\SharpDX.Direct3D9.dll - - - ..\packages\SharpDX.DXGI.4.2.0\lib\net40\SharpDX.DXGI.dll - - - - - - - - - - + + + + + + + + + + + + - - - - - - PreserveNewest @@ -129,7 +78,6 @@ - PreserveNewest @@ -280,24 +228,11 @@ PreserveNewest - - PreserveNewest - - - - - - - - - - - PreserveNewest @@ -1105,14 +1040,8 @@ - - {43b35c43-bd28-4731-81aa-772ee71c08b2} - DebugParser - - - {D75528F5-54F9-4E31-94F6-03C194F74CF2} - DXDecompiler - + + @@ -1272,21 +1201,4 @@ PreserveNewest - - - - - - This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. - - - - - \ No newline at end of file diff --git a/src/DXDecompiler.Tests/Properties/AssemblyInfo.cs b/src/DXDecompiler.Tests/Properties/AssemblyInfo.cs index 4166b3b1..e6beb041 100644 --- a/src/DXDecompiler.Tests/Properties/AssemblyInfo.cs +++ b/src/DXDecompiler.Tests/Properties/AssemblyInfo.cs @@ -1,18 +1,6 @@ using System.Reflection; using System.Runtime.InteropServices; -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("SlimShader.Tests")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("SlimShader.Tests")] -[assembly: AssemblyCopyright("Copyright © 2012")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - // Setting ComVisible to false makes the types in this assembly not visible // to COM components. If you need to access a type in this assembly from // COM, set the ComVisible attribute to true on that type. @@ -20,16 +8,3 @@ // The following GUID is for the ID of the typelib if this project is exposed to COM [assembly: Guid("27e58db9-828f-4641-a0a5-50f283be5fda")] - -// Version information for an assembly consists of the following four values: -// -// Major Version -// Minor Version -// Build Number -// Revision -// -// You can specify all the values or you can default the Build and Revision Numbers -// by using the '*' as shown below: -// [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/src/DXDecompiler.Tests/TestFixtures/DX9Tests.cs b/src/DXDecompiler.Tests/TestFixtures/DX9Tests.cs index ddf6b5f6..cbee2019 100644 --- a/src/DXDecompiler.Tests/TestFixtures/DX9Tests.cs +++ b/src/DXDecompiler.Tests/TestFixtures/DX9Tests.cs @@ -3,7 +3,7 @@ using DXDecompiler.DX9Shader; using DXDecompiler.Tests.Util; using NUnit.Framework; -using SharpDX.D3DCompiler; +using SharpDX.Direct3D9; using System; using System.Collections.Generic; using System.IO; @@ -46,7 +46,7 @@ private static IEnumerable TestShaders /// /// Compare ASM output produced by fxc.exe and SlimShader. /// - [TestCaseSource("TestShaders")] + [TestCaseSource(nameof(TestShaders))] public void AsmMatchesFxc(string relPath) { string file = $"{ShaderDirectory}/{relPath}"; @@ -78,7 +78,7 @@ public void AsmMatchesFxc(string relPath) /// /// Compare ASM output produced by fxc.exe and SlimShader. /// - [TestCaseSource("TestShaders")] + [TestCaseSource(nameof(TestShaders))] public void Decompile(string relPath) { string file = $"{ShaderDirectory}/{relPath}"; @@ -93,7 +93,7 @@ public void Decompile(string relPath) // Assert. } - [TestCaseSource("TestShaders")] + [TestCaseSource(nameof(TestShaders))] public void DumpStructure(string relPath) { string file = $"{ShaderDirectory}/{relPath}"; @@ -181,17 +181,25 @@ public void RecompileShaders(string relPath) // Act. var binaryFileBytes = File.ReadAllBytes(file + ".o"); var shaderModel = ShaderReader.ReadShader(binaryFileBytes); - var decompiledHLSL = HlslWriter.Decompile(shaderModel); + var decompiledHLSL = HlslWriter.Decompile(shaderModel, "main"); File.WriteAllText($"{OutputDir}/{relPath}.d.hlsl", decompiledHLSL); using(var shaderBytecode = ShaderBytecode.FromStream(new MemoryStream(binaryFileBytes))) { - var profile = shaderModel.Type == DX9Shader.ShaderType.Pixel ? - $"ps_{shaderModel.MajorVersion}_{shaderModel.MinorVersion}" : - $"vs_{shaderModel.MajorVersion}_{shaderModel.MinorVersion}"; - var compiledShader = ShaderBytecode.Compile(decompiledHLSL, "main", profile); - var disassembly = shaderBytecode.Disassemble(); + string profile; + switch(shaderModel.Type) + { + case ShaderType.Effect: + profile = "fx_2_0"; + break; + default: + profile = shaderModel.Profile; + break; + } + + var compiledShader = ShaderBytecode.Compile(decompiledHLSL, "main", profile, default); var redisassembly = compiledShader.Bytecode.Disassemble(); + var disassembly = shaderBytecode.Disassemble(); File.WriteAllText($"{OutputDir}/{relPath}.d1.asm", disassembly); File.WriteAllText($"{OutputDir}/{relPath}.d2.asm", redisassembly); diff --git a/src/DXDecompiler.Tests/packages.config b/src/DXDecompiler.Tests/packages.config deleted file mode 100644 index 44a3a3d3..00000000 --- a/src/DXDecompiler.Tests/packages.config +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - \ No newline at end of file diff --git a/src/DXDecompiler/DX9Shader/Asm/AsmWriter.cs b/src/DXDecompiler/DX9Shader/Asm/AsmWriter.cs index 434ef8fa..dc26ae22 100644 --- a/src/DXDecompiler/DX9Shader/Asm/AsmWriter.cs +++ b/src/DXDecompiler/DX9Shader/Asm/AsmWriter.cs @@ -1,6 +1,8 @@ using DXDecompiler.DX9Shader.Asm; using DXDecompiler.DX9Shader.Bytecode.Ctab; +using DXDecompiler.DX9Shader.Decompiler; using System; +using System.IO; using System.Linq; namespace DXDecompiler.DX9Shader @@ -29,6 +31,23 @@ public static string Disassemble(ShaderModel shaderModel) var asmWriter = new AsmWriter(shaderModel); return asmWriter.Decompile(); } + + public string Disassemble(InstructionToken instruction) + { + using var writer = new StringWriter(); + var previous = Writer; + try + { + Writer = writer; + WriteInstruction(instruction); + return writer.ToString(); + } + finally + { + Writer = previous; + } + } + static string ApplyModifier(SourceModifier modifier, string value) { switch(modifier) @@ -90,7 +109,7 @@ string GetSourceName(InstructionToken instruction, int srcIndex, bool isLogicalI { // compute the actual data index, which might be different from logical index // because of relative addressing mode. - + // TODO: Handle relative addressing mode in a better way, // by using `InstructionToken.Operands`: // https://github.com/spacehamster/DXDecompiler/pull/6#issuecomment-782958769 @@ -193,16 +212,8 @@ public void WriteConstantTable(ConstantTable constantTable) WriteLine("//"); foreach(var declaration in constantTable.ConstantDeclarations) { - string arraySubscript = ""; - if(declaration.Elements > 1) - { - arraySubscript = $"[{declaration.Elements}]"; - } - WriteLine("// {0} {1}{2};", - declaration.GetTypeName(), - declaration.Name, - arraySubscript - ); + var decompiled = ConstantTypeWriter.Decompile(declaration.Type, declaration.Name, false, Indent); + WriteLine(string.Join("\n", decompiled.Split('\n').Select(l => $"// {l}"))); } WriteLine("//"); WriteLine("//"); @@ -615,7 +626,7 @@ private void WriteInstruction(InstructionToken instruction) WriteLine("ret"); break; case Opcode.Label: - WriteLine("label", GetSourceName(instruction, 0)); + WriteLine("label {0}", GetSourceName(instruction, 0)); break; case Opcode.Comment: case Opcode.End: diff --git a/src/DXDecompiler/DX9Shader/Asm/EffectAsmWriter.cs b/src/DXDecompiler/DX9Shader/Asm/EffectAsmWriter.cs index 86c02029..6bac9a22 100644 --- a/src/DXDecompiler/DX9Shader/Asm/EffectAsmWriter.cs +++ b/src/DXDecompiler/DX9Shader/Asm/EffectAsmWriter.cs @@ -1,5 +1,7 @@ -using DXDecompiler.DX9Shader.Bytecode.Ctab; +using DXDecompiler.DX9Shader.Bytecode.Ctab; +using DXDecompiler.DX9Shader.Bytecode.Fxlvm; using DXDecompiler.DX9Shader.FX9; +using System; using System.Linq; namespace DXDecompiler.DX9Shader @@ -178,7 +180,36 @@ public void WriteAssignment(Assignment assignment) } else if(stateBlob.BlobType == StateBlobType.IndexShader) { - WriteLine("{0}[TODO];", stateBlob.VariableName); + var variable = EffectChunk.Variables.Find(x => x.Parameter.Name == stateBlob.VariableName); + + var indexer = stateBlob.Shader; + // TODO: Actually the index should be calculated by executing the index shader + var index = 0; + try + { + index = (int)Fx9FxlVm.Execute(indexer)[0]; + } + catch(Exception e) + { + Write($"/* failed to execute index shader: {e.Message} */"); + } + + WriteLine(); + Indent++; + WriteIndent(); + WriteLine("asm {"); + + var variableBlobIndex = variable.DefaultValue[index].UInt; + var variableBlob = EffectChunk.VariableBlobs.Find(x => x.Index == variableBlobIndex); + + var disasm = string.Join("\n", AsmWriter.Disassemble(variableBlob.Shader) + .Split('\n') + .Select(l => $"{new string(' ', Indent * 4)}{l}")); + WriteLine(disasm); + + WriteIndent(); + WriteLine("};"); + Indent--; } } else if(assignment.Type == StateType.VertexShader) diff --git a/src/DXDecompiler/DX9Shader/Asm/PreshaderAsmWriter.cs b/src/DXDecompiler/DX9Shader/Asm/PreshaderAsmWriter.cs index a8e10681..6050b668 100644 --- a/src/DXDecompiler/DX9Shader/Asm/PreshaderAsmWriter.cs +++ b/src/DXDecompiler/DX9Shader/Asm/PreshaderAsmWriter.cs @@ -42,7 +42,7 @@ private void WriteInstruction(FxlcToken instruction) } string FormatOperand(FxlcOperand operand) { - return operand.FormatOperand(Preshader.ConstantTable, Preshader.Cli); + return operand.FormatOperand(Preshader.Cli, null); } } } diff --git a/src/DXDecompiler/DX9Shader/Bytecode/CliToken.cs b/src/DXDecompiler/DX9Shader/Bytecode/CliToken.cs index 1dc75fe3..4f9d53b4 100644 --- a/src/DXDecompiler/DX9Shader/Bytecode/CliToken.cs +++ b/src/DXDecompiler/DX9Shader/Bytecode/CliToken.cs @@ -1,4 +1,5 @@ using DXDecompiler.Util; +using System; using System.Collections.Generic; namespace DXDecompiler.DX9Shader.Bytecode @@ -22,7 +23,16 @@ public static CliToken Parse(BytecodeReader reader) public string GetLiteral(uint index) { - return Numbers[(int)index].ToString(); + var value = Numbers[(int)index]; + // TODO: Upgrade to .NET Core 3.0 or above so we can avoid this explicit check + // https://stackoverflow.com/a/62768234/4399840 + if(BitConverter.DoubleToInt64Bits(value) == BitConverter.DoubleToInt64Bits(-0.0)) + { + return "-0"; + } + return value.ToString(); } + + public float GetLiteralAsFloat(uint index) => (float)Numbers[(int)index]; } } diff --git a/src/DXDecompiler/DX9Shader/Bytecode/Ctab/ConstantDeclaration.cs b/src/DXDecompiler/DX9Shader/Bytecode/Ctab/ConstantDeclaration.cs index 05f1f527..847e9e70 100644 --- a/src/DXDecompiler/DX9Shader/Bytecode/Ctab/ConstantDeclaration.cs +++ b/src/DXDecompiler/DX9Shader/Bytecode/Ctab/ConstantDeclaration.cs @@ -19,6 +19,7 @@ public class ConstantDeclaration public ushort RegisterCount { get; private set; } public ConstantType Type { get; private set; } public List DefaultValue { get; private set; } + public List DefaultValueWithPadding { get; private set; } public uint Rows => Type.Rows; public uint Columns => Type.Columns; @@ -45,7 +46,8 @@ public ConstantDeclaration(string name, RegisterSet registerSet, short registerI } public ConstantDeclaration() { - DefaultValue = new List(); + DefaultValue = new(); + DefaultValueWithPadding = new(); } public static ConstantDeclaration Parse(BytecodeReader reader, BytecodeReader decReader) { @@ -68,9 +70,50 @@ public static ConstantDeclaration Parse(BytecodeReader reader, BytecodeReader de { //Note: thre are corrisponding def instructions. TODO: check that they are the same var defaultValueReader = reader.CopyAtOffset((int)defaultValueOffset); - for(int i = 0; i < 4; i++) + if(result.Type.ParameterClass == ParameterClass.Object) { - result.DefaultValue.Add(defaultValueReader.ReadSingle()); + for(int i = 0; i < 4; i++) + { + var value = defaultValueReader.ReadSingle(); + result.DefaultValueWithPadding.Add(value); + result.DefaultValue.Add(value); + } + } + else + { + var forever = uint.MaxValue; + var elementCounter = 0; + result.TraverseChildTree(ref forever, (type, name, index, depth) => + { + float[] ReadFour() + { + var values = new float[4]; + for(int i = 0; i < 4; ++i) + { + values[i] = defaultValueReader.ReadSingle(); + } + result.DefaultValueWithPadding.AddRange(values); + return values; + } + + switch(type.ParameterClass) + { + case ParameterClass.Struct: + break; + case ParameterClass.MatrixColumns: + result.DefaultValue.AddRange(ReadFour().Take((int)type.Rows)); + break; + case ParameterClass.MatrixRows: + case ParameterClass.Vector: + result.DefaultValue.AddRange(ReadFour().Take((int)type.Columns)); + break; + case ParameterClass.Scalar: + result.DefaultValue.Add(ReadFour()[0]); + break; + default: + throw new NotSupportedException(type.ParameterClass.ToString()); + } + }); } } return result; @@ -93,6 +136,63 @@ public string GetRegisterName() return $"{RegisterSet.GetDescription()}{RegisterIndex}"; } + public string GetConstantNameByRegisterNumber(uint registerNumber, string relativeAddressing) + { + var decl = this; + var totalOffset = registerNumber - decl.RegisterIndex; + var data = decl.GetRegisterTypeByOffset(totalOffset); + var name = decl.GetMemberNameByOffset(totalOffset); + var offsetFromMember = registerNumber - data.RegisterIndex; + + if(relativeAddressing != null) + { + // a nasty way to check array subscripts + // TODO: we need something less ugly... + var firstDotIndex = name.IndexOf('.'); + firstDotIndex = firstDotIndex == -1 ? name.Length : name.Length; + var isChildArray = name.LastIndexOf(']') > firstDotIndex; + if(decl.Elements <= 1 || isChildArray || name.Count(x => x == ']') != 1) + { + // we cannot handle relative addressing if this contant declaration is not an array + // we also cannot handle relative addressing if this constant declaration contains nested arrays + name = $"Error({name}, {relativeAddressing})"; + } + else + { + var declElementSize = decl.RegisterCount / decl.Elements; + if(declElementSize > 1) + { + relativeAddressing = $"({relativeAddressing} / {declElementSize})"; + } + name = name.Replace("]", $" + {relativeAddressing}]"); + } + } + + // how many registers should be occupied by the current constant item + var registersOccupied = data.Type.ParameterClass == ParameterClass.MatrixColumns + ? data.Type.Columns + : data.Type.Rows; + // sanity check: if the current constant item only occupies one register + // then its start index must match with the register number + if(registersOccupied == 1 && data.RegisterIndex != registerNumber) + { + throw new InvalidOperationException(); + } + + // matrix row / columns + if(data.Type.ParameterClass == ParameterClass.MatrixRows) + { + return $"{name}[{offsetFromMember}]"; + } + else if(data.Type.ParameterClass == ParameterClass.MatrixColumns) + { + var accessors = Enumerable.Range(0, (int)data.Type.Rows).Select(i => $"m{i}{offsetFromMember}"); + return $"({name}._{string.Join("_", accessors)})"; + } + + return name; + } + public class ConstantRegisterData { public ConstantType Type { get; } @@ -153,6 +253,7 @@ public string GetMemberNameByOffset(uint offset) var lastDepth = 0; TraverseChildTree(ref offset, (type, name, index, depth) => { + name = name.TrimStart('$'); if(depth >= indexes.Count) { indexes.Add((name, index, type.Elements)); @@ -190,8 +291,9 @@ public string GetMemberNameByOffset(uint offset) /// The type of current target. /// /// The offset of child element to be searched. - /// It will become 0 if a child element has been found exactly on the specified offset, - /// non-zero if we are accessing "in the middle of an element", + /// If it's , then it will traverse the entire tree.
+ /// Otherwise, it will become 0 if a child element has been found exactly on the specified offset, + /// or non-zero if we are accessing "in the middle of an element", /// something like "accessing the 2nd column of matrix". /// /// @@ -199,8 +301,9 @@ public string GetMemberNameByOffset(uint offset) /// private void TraverseChildTree(ref uint offset, ChildTreeVisitor visitor) { + var fullTraverse = offset == uint.MaxValue; var found = TraverseChildTree(Name, Type, ref offset, visitor, 0); - if(!found) + if(!found && !fullTraverse) { throw new InvalidOperationException(); } diff --git a/src/DXDecompiler/DX9Shader/Bytecode/EnumExtensions.cs b/src/DXDecompiler/DX9Shader/Bytecode/EnumExtensions.cs index 544291e6..843f1247 100644 --- a/src/DXDecompiler/DX9Shader/Bytecode/EnumExtensions.cs +++ b/src/DXDecompiler/DX9Shader/Bytecode/EnumExtensions.cs @@ -23,6 +23,8 @@ public static string GetDescription(this ParameterType value) return "texture2D"; case ParameterType.Texture3D: return "texture3D"; + case ParameterType.TextureCube: + return "textureCUBE"; default: return value.ToString().ToLower(); } @@ -163,5 +165,216 @@ public static bool IsTextureOperation(this Opcode opcode) return false; } } + public static bool ReturnsScalar(this Opcode opcode) + { + switch(opcode) + { + // TODO: there could be more parallel instructions + case Opcode.DP2Add: + case Opcode.Dp3: + case Opcode.Dp4: + return true; + default: + return false; + } + } + /// + /// Check if the instruction is "parallel", that is, + /// each component will be processed independently without affecting other components. + ///
+ /// This is useful for clipping source swizzles with destination write masks: + /// Since the instruction is parallel, it's safe to clip source swizzles as they will not have any effect. + ///
+ /// The instruction to be checked + /// The current shader + /// true if instruction is parallel + public static bool IsParallel(this Opcode opcode, ShaderModel shader) + { + switch(opcode) + { + // TODO: there could be more parallel instructions + case Opcode.Abs: + case Opcode.Add: + case Opcode.Cmp: + case Opcode.Cnd when shader is { MajorVersion: 1, MinorVersion: >= 4 } or { MajorVersion: > 1 }: + case Opcode.DSX: + case Opcode.DSY: + case Opcode.Exp: + case Opcode.ExpP: + case Opcode.Frc: + case Opcode.Log: + case Opcode.LogP: + case Opcode.Lrp: + case Opcode.Mad: + case Opcode.Max: + case Opcode.Min: + case Opcode.Mov: + case Opcode.MovA when shader.MajorVersion >= 2: + case Opcode.Mul: + case Opcode.Pow: + case Opcode.Rcp: + case Opcode.Rsq: + case Opcode.SetP: + case Opcode.Sge: + case Opcode.Sgn: + case Opcode.Slt: + case Opcode.Sub: + case Opcode.TexKill: + return true; + default: + return false; + } + } + /// + /// Get the size of instruction in shader model 1. + /// + /// The opcode of the instruction + /// The number of tokens inside this instruction. + public static int GetShaderModel1OpcodeSize(this Opcode opcode, int minorVersion) + { + switch(opcode) + { + case Opcode.Abs: + return 2; + case Opcode.Add: + return 3; + case Opcode.Bem: + return 3; + case Opcode.Breakp: + return 1; + case Opcode.Cmp: + return 4; + case Opcode.Cnd: + return 4; + case Opcode.Crs: + return 3; + case Opcode.Dcl: + return 2; + case Opcode.Def: + return 5; + case Opcode.DefB: + return 2; + case Opcode.DefI: + return 5; + case Opcode.Dp3: + return 3; + case Opcode.Dp4: + return 3; + case Opcode.Dst: + return 3; + case Opcode.Exp: + return 2; + case Opcode.ExpP: + return 2; + case Opcode.Frc: + return 2; + case Opcode.Lit: + return 2; + case Opcode.Log: + return 2; + case Opcode.LogP: + return 2; + case Opcode.Lrp: + return 4; + case Opcode.M3x2: + return 3; + case Opcode.M3x3: + return 3; + case Opcode.M3x4: + return 3; + case Opcode.M4x3: + return 3; + case Opcode.M4x4: + return 3; + case Opcode.Mad: + return 4; + case Opcode.Max: + return 3; + case Opcode.Min: + return 3; + case Opcode.Mov: + return 2; + case Opcode.Mul: + return 3; + case Opcode.Nop: + return 0; + case Opcode.Nrm: + return 2; + case Opcode.Phase: + return 0; + case Opcode.Pow: + return 3; + case Opcode.Rcp: + return 2; + case Opcode.Rsq: + return 2; + case Opcode.SetP: + return 3; + case Opcode.Sge: + return 3; + case Opcode.Slt: + return 3; + case Opcode.Sub: + return 3; + case Opcode.Tex: + if(minorVersion < 4) + { + return 1; + } + else + { + return 2; + } + case Opcode.TexBem: + return 2; + case Opcode.TexBeml: + return 2; + case Opcode.TexCoord: + if(minorVersion < 4) + { + return 1; + } + else + { + return 2; + } + case Opcode.TexDepth: + return 1; + case Opcode.TexDP3: + return 2; + case Opcode.TexDP3Tex: + return 2; + case Opcode.TexKill: + return 1; + case Opcode.TexM3x2Depth: + return 2; + case Opcode.TeXM3x2Pad: + return 2; + case Opcode.TexM3x2Tex: + return 2; + case Opcode.TexM3x3: + return 2; + case Opcode.TexM3x3Diff: + return 2; + case Opcode.TeXM3x3Pad: + return 2; + case Opcode.TexM3x3Spec: + return 3; + case Opcode.TexM3x3Tex: + return 2; + case Opcode.TexM3x3VSpec: + return 2; + case Opcode.TexReg2AR: + return 2; + case Opcode.TexReg2GB: + return 2; + case Opcode.TexReg2RGB: + return 2; + case Opcode.End: + return 0; + default: + throw new NotImplementedException(opcode.ToString()); + } + } } } diff --git a/src/DXDecompiler/DX9Shader/Bytecode/Fxlvm/Fx9FxlVm.cs b/src/DXDecompiler/DX9Shader/Bytecode/Fxlvm/Fx9FxlVm.cs new file mode 100644 index 00000000..e88d3311 --- /dev/null +++ b/src/DXDecompiler/DX9Shader/Bytecode/Fxlvm/Fx9FxlVm.cs @@ -0,0 +1,184 @@ +using DXDecompiler.DX9Shader.Bytecode.Ctab; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace DXDecompiler.DX9Shader.Bytecode.Fxlvm +{ + /// + /// Mainly used to execute index shaders and select the default-displayed shader in effect disassembly + /// + class Fx9FxlVm + { + private abstract class VmOperand + { + public int Start { get; } + public int Count { get; } + + public VmOperand(FxlcOperand operand) + { + Start = (int)operand.OpIndex; + Count = (int)operand.ComponentCount; + } + public abstract void Assign(IEnumerable values); + public abstract IEnumerable Retrieve(); + } + + private readonly Dictionary _expressions = new(); + private readonly Dictionary _temporaries = new(); + private readonly List _tokens; + private readonly ConstantTable _ctab; + private readonly CliToken _cli; + + public static List Execute(ShaderModel shader) + { + return new Fx9FxlVm(shader.Fxlc, shader.ConstantTable, shader.Cli).Execute(); + } + + private Fx9FxlVm(FxlcBlock block, ConstantTable ctab, CliToken cli) + { + _tokens = block.Tokens; + _ctab = ctab; + _cli = cli; + } + + private VmOperand MakeOperand(FxlcOperand operand) + { + return operand.OpType switch + { + FxlcOperandType.Expr => new TempOperand(operand, _expressions), + FxlcOperandType.Temp => new TempOperand(operand, _temporaries), + FxlcOperandType.Variable => new CtabOperand(operand, _ctab), + FxlcOperandType.Literal => new LiteralOperand(operand, _cli), + _ => throw new NotImplementedException() + }; + } + + private List Execute() + { + foreach(var token in _tokens) + { + var destination = MakeOperand(token.Operands[0]); + + switch(token.Opcode) + { + case FxlcOpcode.Mov: + destination.Assign(MakeOperand(token.Operands[1]).Retrieve()); + break; + case FxlcOpcode.Add: + var a = MakeOperand(token.Operands[1]).Retrieve(); + var b = MakeOperand(token.Operands[2]).Retrieve(); + destination.Assign(a.Zip(b, (a, b) => a + b)); + break; + default: + throw new NotImplementedException(token.Opcode.ToString()); + } + + } + var result = new List(); + for(var i = 0; i < 4; ++i) + { + _expressions.TryGetValue(i, out var value); + result.Add(value); + } + return result; + } + + private class TempOperand : VmOperand + { + private readonly Dictionary _source; + + public TempOperand(FxlcOperand operand, Dictionary source) : base(operand) + { + _source = source; + } + + public override void Assign(IEnumerable values) + { + var list = values.ToList(); + var end = Start + Count; + for(var i = Start; i < end; ++i) + { + _source[i] = list[i % list.Count]; + } + } + + public override IEnumerable Retrieve() + { + var end = Start + Count; + for(var i = Start; i < end; ++i) + { + _source.TryGetValue(i, out var value); + yield return value; + } + } + } + + private class CtabOperand : VmOperand + { + private readonly ConstantTable _source; + private readonly RegisterSet _registerSet; + + public CtabOperand(FxlcOperand operand, ConstantTable table) : base(operand) + { + _source = table; + // TODO + _registerSet = RegisterSet.Float4; + if(operand.IsArray != 0) + { + throw new NotImplementedException(); + } + } + + public override void Assign(IEnumerable values) => throw new NotSupportedException(); + + public override IEnumerable Retrieve() + { + List current = null; + int offset = 0; + var end = Start + Count; + for(var i = Start; i < end; ++i) + { + if(current == null || (offset + i) < current.Count) + { + foreach(var x in _source.ConstantDeclarations) + { + if(x.RegisterSet != _registerSet) + { + continue; + } + var constantDecarationStart = x.RegisterIndex * 4; + var constantValueCount = x.RegisterCount * 4; + if(i < constantDecarationStart || i >= (constantDecarationStart + constantValueCount)) + { + continue; + } + if(x.DefaultValueWithPadding.Count != constantValueCount) + { + throw new InvalidOperationException(); + } + offset = -constantDecarationStart; + current = x.DefaultValueWithPadding; + } + } + + yield return current[offset + i]; + } + } + } + + private class LiteralOperand : VmOperand + { + public IEnumerable Literals { get; } + + public LiteralOperand(FxlcOperand operand, CliToken cli) : base(operand) + { + Literals = Enumerable.Repeat(cli.GetLiteralAsFloat(operand.OpIndex), (int)operand.ComponentCount); + } + + public override void Assign(IEnumerable values) => throw new NotSupportedException(); + + public override IEnumerable Retrieve() => Literals; + } + } +} diff --git a/src/DXDecompiler/DX9Shader/Bytecode/Fxlvm/FxlcOperand.cs b/src/DXDecompiler/DX9Shader/Bytecode/Fxlvm/FxlcOperand.cs index 139282e9..78be297e 100644 --- a/src/DXDecompiler/DX9Shader/Bytecode/Fxlvm/FxlcOperand.cs +++ b/src/DXDecompiler/DX9Shader/Bytecode/Fxlvm/FxlcOperand.cs @@ -1,6 +1,7 @@ using DXDecompiler.DX9Shader.Bytecode.Ctab; using DXDecompiler.Util; using System; +using System.Collections.Generic; using System.Diagnostics; using System.Linq; @@ -13,8 +14,9 @@ public class FxlcOperand public uint OpIndex { get; private set; } public FxlcOperandType ArrayType { get; private set; } public uint ArrayIndex { get; private set; } + public uint ComponentCount { get; private set; } + public bool IsScalar { get; private set; } - private uint ComponentCount; public static FxlcOperand Parse(BytecodeReader reader, uint componentCount, bool isScalarOp) { var result = new FxlcOperand() @@ -22,6 +24,7 @@ public static FxlcOperand Parse(BytecodeReader reader, uint componentCount, bool IsArray = reader.ReadUInt32(), OpType = (FxlcOperandType)reader.ReadUInt32(), OpIndex = reader.ReadUInt32(), + IsScalar = isScalarOp }; result.ComponentCount = isScalarOp && result.OpType != FxlcOperandType.Literal ? 1 : componentCount; Debug.Assert(Enum.IsDefined(typeof(FxlcOperandType), result.OpType), @@ -104,25 +107,55 @@ private string FormatComponent(uint componentIndex, uint componentCount) return $".UnknownCount{componentCount}"; } } - private string FormatOperand(ConstantTable ctab, CliToken cli, FxlcOperandType type, uint index) + private string FormatOperand( + ConstantTable ctab, + CliToken cli, + HashSet ctabOverride, + FxlcOperandType type, + uint index, + string indexer, + out string component) { var elementIndex = index / 4; var componentIndex = index % 4; - var component = FormatComponent(componentIndex, ComponentCount); + component = FormatComponent(componentIndex, ComponentCount); switch(type) { case FxlcOperandType.Literal: - var literal = string.Join(", ", - Enumerable.Repeat(cli.GetLiteral(index), (int)ComponentCount)); - return string.Format("({0})", literal); + var literal = cli.GetLiteral(index); + var firstLiteral = literal is "-0" ? "0" : literal; + for(var i = 1u; i < ComponentCount; ++i) + { + var text = IsScalar + ? firstLiteral + : cli.GetLiteral(index + i); + literal += $", {text}"; + } + component = string.Empty; + var typeName = ctab is null || ComponentCount == 1 ? string.Empty : $"float{ComponentCount}"; + return $"{typeName}({literal})"; case FxlcOperandType.Temp: - return string.Format("r{0}{1}", elementIndex, component); + return $"{(ctab is null ? "r" : "temp")}{elementIndex}"; case FxlcOperandType.Variable: - return string.Format("c{0}{1}", elementIndex, component); + if(ctabOverride?.Contains(elementIndex) is true) + { + return $"expr{elementIndex}"; + } + if(ctab is not null) + { + return ctab.ConstantDeclarations + .First(d => d.ContainsIndex(elementIndex)) + .GetConstantNameByRegisterNumber(elementIndex, indexer); + } + else if(!string.IsNullOrEmpty(indexer)) + { + return $"c{elementIndex}[{indexer}]"; + } + return $"c{elementIndex}"; case FxlcOperandType.Expr: - return string.Format("c{0}{1}", elementIndex, component); + return $"{(ctab is null ? "c" : "expr")}{elementIndex}"; default: - return string.Format("unknown{0}{1}", elementIndex, component); + return $"unknown{elementIndex}"; ; } } private string FormatOperand(ConstantTable ctab, Chunks.Fxlvm.Cli4Chunk cli, FxlcOperandType type, uint index) @@ -159,20 +192,21 @@ private string FormatOperand(ConstantTable ctab, Chunks.Fxlvm.Cli4Chunk cli, Fxl /// /// Format operand for FX9 preshaders /// - /// - /// - /// - public string FormatOperand(ConstantTable ctab, CliToken cli) + /// CliToken, neccessary for retrieving literal values + /// ConstantTable, optional. If not null, it will be used to resolve constants. + /// Constant registers overwritten by preshader + /// The formatted operand as string + public string FormatOperand(CliToken cli, ConstantTable ctab, HashSet ctabOverride = null) { if(IsArray == 0) { - return FormatOperand(ctab, cli, OpType, OpIndex); + return FormatOperand(ctab, cli, ctabOverride, OpType, OpIndex, null, out var component) + component; } else { - return string.Format("{0}[{1}]", - FormatOperand(ctab, cli, ArrayType, ArrayIndex), - FormatOperand(ctab, cli, OpType, OpIndex)); + var indexOperand = FormatOperand(ctab, cli, ctabOverride, OpType, OpIndex, null, out _) + ".x"; + var arrayOperand = FormatOperand(ctab, cli, ctabOverride, ArrayType, ArrayIndex, indexOperand, out var elementComponent); + return string.Format("{0}{1}", arrayOperand, elementComponent); } } /// diff --git a/src/DXDecompiler/DX9Shader/Bytecode/Fxlvm/FxlcToken.cs b/src/DXDecompiler/DX9Shader/Bytecode/Fxlvm/FxlcToken.cs index def24a5d..5ea4bb25 100644 --- a/src/DXDecompiler/DX9Shader/Bytecode/Fxlvm/FxlcToken.cs +++ b/src/DXDecompiler/DX9Shader/Bytecode/Fxlvm/FxlcToken.cs @@ -36,7 +36,7 @@ public static FxlcToken Parse(BytecodeReader reader) for(int i = 0; i < operandCount; i++) { var isScalarOp = i == 0 && result.IsScalarInstruction; - result.Operands.Add(FxlcOperand.Parse(reader, tokenComponentCount, isScalarOp)); + result.Operands.Add(FxlcOperand.Parse(reader, tokenComponentCount, isScalarOp && i == 0)); } // destination operand result.Operands.Insert(0, FxlcOperand.Parse(reader, tokenComponentCount, false)); @@ -56,9 +56,9 @@ public override string ToString() operands); } - public string ToString(ConstantTable ctab, CliToken cli) + public string ToString(CliToken cli) { - var operands = string.Join(", ", Operands.Select(o => o.FormatOperand(ctab, cli))); + var operands = string.Join(", ", Operands.Select(o => o.FormatOperand(cli, null))); return string.Format("{0} {1}", Opcode.ToString().ToLowerInvariant(), operands); diff --git a/src/DXDecompiler/DX9Shader/Bytecode/InstructionToken.cs b/src/DXDecompiler/DX9Shader/Bytecode/InstructionToken.cs index a92c5dbe..6595957b 100644 --- a/src/DXDecompiler/DX9Shader/Bytecode/InstructionToken.cs +++ b/src/DXDecompiler/DX9Shader/Bytecode/InstructionToken.cs @@ -342,46 +342,41 @@ public byte[] GetSourceSwizzleComponents(int srcIndex) return swizzleArray; } - public string GetSourceSwizzleName(int srcIndex, bool hlsl = false) + public int? GetSourceSwizzleLimit(int operandDataIndex) { - int swizzleLength = 4; - if(Opcode == Opcode.Dp4) + int logicalIndex = 0; + for(var i = 0; i < operandDataIndex; ++i) { - swizzleLength = 4; + if(!IsRelativeAddressMode(i)) + { + ++logicalIndex; + } } + + return Opcode switch + { + Opcode.Dp3 => 3, + Opcode.DP2Add => logicalIndex < 3 ? 2 : 1,// dp2add dst, src0.xy src1.xy src2.x + _ => null, + }; + } + + public string GetSourceSwizzleName(int srcIndex, bool hlsl = false) + { + int? swizzleLimit = null; //TODO: Probably useful in hlsl mode - else if(hlsl) + if(hlsl) { - if(Opcode == Opcode.Dp3) - { - swizzleLength = 3; - } - else if(HasDestination) - { - swizzleLength = GetDestinationMaskLength(); - } + swizzleLimit = GetSourceSwizzleLimit(srcIndex); } string swizzleName = ""; byte[] swizzle = GetSourceSwizzleComponents(srcIndex); - for(int i = 0; i < swizzleLength; i++) + for(int i = 0; i < (swizzleLimit ?? 4); i++) { - switch(swizzle[i]) - { - case 0: - swizzleName += "x"; - break; - case 1: - swizzleName += "y"; - break; - case 2: - swizzleName += "z"; - break; - case 3: - swizzleName += "w"; - break; - } + swizzleName += "xyzw"[swizzle[i]]; } + switch(swizzleName) { case "xxx": @@ -390,7 +385,7 @@ public string GetSourceSwizzleName(int srcIndex, bool hlsl = false) return ".y"; case "zzz": return ".z"; - case "xyz": + case "xyz" when swizzleLimit is null: return ""; case "xyzw": return ""; @@ -443,6 +438,19 @@ public string GetDeclSemantic() : GetDeclIndex().ToString(); switch(registerType) { + case RegisterType.Input when _shaderModel is { Type: ShaderType.Pixel, MajorVersion: <= 2 }: + case RegisterType.Texture when _shaderModel is { Type: ShaderType.Pixel, MajorVersion: <= 2 }: + declIndexString = GetParamRegisterNumber(1) is 0 + ? string.Empty + : GetParamRegisterNumber(1).ToString(); + + // https://docs.microsoft.com/en-us/windows/win32/direct3dhlsl/dcl---ps + return registerType switch + { + RegisterType.Input => "COLOR", + RegisterType.Texture => "TEXCOORD", + _ => throw new NotSupportedException(registerType.ToString()) + } + declIndexString; case RegisterType.Input: case RegisterType.Output when _shaderModel.Type == ShaderType.Vertex && _shaderModel.MajorVersion >= 3: string name; @@ -491,9 +499,6 @@ public string GetDeclSemantic() return "vPos"; } throw new NotImplementedException(); - case RegisterType.Texture when _shaderModel.Type == ShaderType.Pixel && _shaderModel.MajorVersion <= 2: - var registerNumber = GetParamRegisterNumber(1); - return "TEXCOORD" + (registerNumber == 0 ? string.Empty : registerNumber.ToString()); case RegisterType.TexCoordOut: return "TEXCOORD" + declIndexString; default: diff --git a/src/DXDecompiler/DX9Shader/Bytecode/ShaderModel.cs b/src/DXDecompiler/DX9Shader/Bytecode/ShaderModel.cs index 255b0302..9df19664 100644 --- a/src/DXDecompiler/DX9Shader/Bytecode/ShaderModel.cs +++ b/src/DXDecompiler/DX9Shader/Bytecode/ShaderModel.cs @@ -25,6 +25,11 @@ public enum ShaderType public class ShaderModel { + static ShaderModel() + { + System.Globalization.CultureInfo.CurrentCulture = System.Globalization.CultureInfo.InvariantCulture; + } + private static readonly Dictionary KnownCommentTypes = new Dictionary { @@ -46,6 +51,22 @@ public class ShaderModel public PrsiToken Prsi { get; set; } public IEnumerable Instructions => Tokens.OfType(); + public string Profile + { + get + { + var version = $"{MajorVersion}_{MinorVersion}"; + version = version switch + { + "2_1" => "2_a", + "2_255" => "2_sw", + "3_255" => "3_sw", + _ => version + }; + return $"{Type.GetDescription()}_{version}"; + } + } + public ShaderModel(int majorVersion, int minorVersion, ShaderType type) { MajorVersion = majorVersion; @@ -72,9 +93,6 @@ public static ShaderModel Parse(BytecodeReader reader) result.MinorVersion = reader.ReadByte(); result.MajorVersion = reader.ReadByte(); result.Type = (ShaderType)reader.ReadUInt16(); - //SM1 shaders do not encode instruction size which rely on for reading operands. - //So we won't support SM1 - if(result.MajorVersion == 1) throw new ParseException("Shader Model 1 is not supported"); while(true) { var instruction = result.ReadInstruction(reader); @@ -95,7 +113,14 @@ Token ReadInstruction(BytecodeReader reader) } else { - size = (int)((instructionToken >> 24) & 0x0f); + if(MajorVersion > 1) + { + size = (int)((instructionToken >> 24) & 0x0f); + } + else + { + size = opcode.GetShaderModel1OpcodeSize(MinorVersion); + } } Token token = null; if(opcode == Opcode.Comment) @@ -117,7 +142,10 @@ Token ReadInstruction(BytecodeReader reader) Fxlc = FxlcBlock.Parse(commentReader); return null; case CommentType.PRES: - Preshader = Preshader.Parse(commentReader); + if(size > 1) + { + Preshader = Preshader.Parse(commentReader); + } return null; case CommentType.PRSI: Prsi = PrsiToken.Parse(commentReader); @@ -162,7 +190,17 @@ Token ReadInstruction(BytecodeReader reader) if((token.Data[i] & (1 << 13)) != 0) { //Relative Address mode - token.Data[i + 1] = reader.ReadUInt32(); + if(MajorVersion < 2) + { + // special handling for SM1 shaders + token.AddData(); + token.Data[i + 1] = 0xB0000000; + size++; + } + else + { + token.Data[i + 1] = reader.ReadUInt32(); + } inst.Operands.Add(new DestinationOperand(token.Data[i], token.Data[i + 1])); i++; } @@ -174,7 +212,17 @@ Token ReadInstruction(BytecodeReader reader) else if((token.Data[i] & (1 << 13)) != 0) { //Relative Address mode - token.Data[i + 1] = reader.ReadUInt32(); + if(MajorVersion < 2) + { + // special handling for SM1 shaders + token.AddData(); + token.Data[i + 1] = 0xB0000000; + size++; + } + else + { + token.Data[i + 1] = reader.ReadUInt32(); + } inst.Operands.Add(new SourceOperand(token.Data[i], token.Data[i + 1])); i++; } diff --git a/src/DXDecompiler/DX9Shader/Bytecode/Token.cs b/src/DXDecompiler/DX9Shader/Bytecode/Token.cs index 7c395770..7f9a8938 100644 --- a/src/DXDecompiler/DX9Shader/Bytecode/Token.cs +++ b/src/DXDecompiler/DX9Shader/Bytecode/Token.cs @@ -35,6 +35,15 @@ public Token(Opcode opcode, int numParams, ShaderModel shaderModel) _shaderModel = shaderModel; } + public void AddData() + { + int len = Data.Length; + len++; + uint[] d = new uint[len]; + System.Array.Copy(Data, d, Data.Length); + Data = d; + } + public override string ToString() { return Opcode.ToString(); diff --git a/src/DXDecompiler/DX9Shader/DecompileWriter.cs b/src/DXDecompiler/DX9Shader/DecompileWriter.cs index adaa256b..7788b90f 100644 --- a/src/DXDecompiler/DX9Shader/DecompileWriter.cs +++ b/src/DXDecompiler/DX9Shader/DecompileWriter.cs @@ -6,7 +6,7 @@ namespace DXDecompiler.DX9Shader public class DecompileWriter { public int Indent; - StreamWriter Writer; + protected TextWriter Writer; protected void WriteIndent() { Writer.Write(new string(' ', Indent * 4)); diff --git a/src/DXDecompiler/DX9Shader/Decompiler/ConstantTypeWriter.cs b/src/DXDecompiler/DX9Shader/Decompiler/ConstantTypeWriter.cs new file mode 100644 index 00000000..e19bbd73 --- /dev/null +++ b/src/DXDecompiler/DX9Shader/Decompiler/ConstantTypeWriter.cs @@ -0,0 +1,128 @@ +using DXDecompiler.DX9Shader.Bytecode.Ctab; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace DXDecompiler.DX9Shader.Decompiler +{ + public class DecompiledConstantDeclaration + { + public string Name { get; set; } + public string Code { get; set; } + public Dictionary RegisterAssignments { get; } = new(); // example: key = "ps_2_0", value = "c0" + public string DefaultValue { get; set; } + + public string RegisterAssignmentString => + string.Concat(RegisterAssignments.Select(t => $" : register({t.Key}, {t.Value})")); + } + + class ConstantTypeWriter : DecompileWriter + { + private readonly ConstantType _initialType; + private readonly string _initialName; + private readonly bool _addTrailingSemicolon; + private readonly bool _printMatrixOrientation; + + public static string Decompile(ConstantType type, string name, bool isHlsl, int indent = 0) + { + var decompiler = new ConstantTypeWriter(type, name, !isHlsl, isHlsl) + { + Indent = indent + }; + return decompiler.Decompile(); + } + public static DecompiledConstantDeclaration Decompile(ConstantDeclaration declaration, ShaderModel shader) + { + + var defaultValue = declaration.DefaultValue.All(v => v == 0) + ? null + : string.Format("{{ {0} }}", string.Join(", ", declaration.DefaultValue)); + var decompiled = new DecompiledConstantDeclaration + { + Name = declaration.Name, + Code = Decompile(declaration.Type, declaration.Name.TrimStart('$'), true), + DefaultValue = defaultValue, + }; + if(shader.Type != ShaderType.Expression) + { + var register = declaration.RegisterSet.GetDescription() + declaration.RegisterIndex; + decompiled.RegisterAssignments[shader.Profile] = register; + } + return decompiled; + } + private ConstantTypeWriter(ConstantType initialType, string initialName, bool addTrailingSemicolon, bool printMatrixOrientation) + { + _initialType = initialType; + _initialName = initialName; + _addTrailingSemicolon = addTrailingSemicolon; + _printMatrixOrientation = printMatrixOrientation; + } + protected override void Write() + { + Write(_initialType, _initialName, _addTrailingSemicolon); + } + private void Write(ConstantType type, string name, bool addSemicolon = false) + { + string typeName = GetConstantTypeName(type); + WriteIndent(); + Write("{0}", typeName); + if(type.ParameterClass == ParameterClass.Struct) + { + WriteLine(string.Empty); + WriteIndent(); + WriteLine("{"); + Indent++; + foreach(var member in type.Members) + { + Write(member.Type, member.Name, true); + } + Indent--; + WriteIndent(); + Write("}"); + } + Write(" {0}", name); + if(type.Elements > 1) + { + Write("[{0}]", type.Elements); + } + if(addSemicolon) + { + WriteLine(";"); + } + } + private string GetConstantTypeName(ConstantType type) + { + switch(type.ParameterClass) + { + case ParameterClass.Scalar: + return type.ParameterType.GetDescription(); + case ParameterClass.Vector: + return type.ParameterType.GetDescription() + type.Columns; + case ParameterClass.Struct: + return "struct"; + case ParameterClass.MatrixColumns: + case ParameterClass.MatrixRows: + var prefix = string.Empty; + if(_printMatrixOrientation) + { + prefix = type.ParameterClass == ParameterClass.MatrixColumns + ? "column_major " + : "row_major "; + } + return $"{prefix}{type.ParameterType.GetDescription()}{type.Rows}x{type.Columns}"; + case ParameterClass.Object: + switch(type.ParameterType) + { + case ParameterType.Sampler1D: + case ParameterType.Sampler2D: + case ParameterType.Sampler3D: + case ParameterType.SamplerCube: + return type.ParameterType.GetDescription(); + default: + throw new NotImplementedException(); + } + } + throw new NotImplementedException(); + } + } +} diff --git a/src/DXDecompiler/DX9Shader/Decompiler/EffectHLSLWriter.cs b/src/DXDecompiler/DX9Shader/Decompiler/EffectHLSLWriter.cs index 2da480a2..1671f721 100644 --- a/src/DXDecompiler/DX9Shader/Decompiler/EffectHLSLWriter.cs +++ b/src/DXDecompiler/DX9Shader/Decompiler/EffectHLSLWriter.cs @@ -9,11 +9,14 @@ namespace DXDecompiler.DX9Shader { public class EffectHLSLWriter : DecompileWriter { - EffectContainer EffectChunk; - Dictionary ShaderNames = new Dictionary(); + private readonly Dictionary _shaderNames = new(); + private readonly EffectContainer _effectChunk; + + public Dictionary CommonConstantDeclarations { get; } = new(); + public EffectHLSLWriter(EffectContainer effectChunk) { - EffectChunk = effectChunk; + _effectChunk = effectChunk; } public static string Decompile(EffectContainer effectChunk) { @@ -22,32 +25,153 @@ public static string Decompile(EffectContainer effectChunk) } void BuildNameLookup() { - int shaderCount = 0; - foreach(var blob in EffectChunk.VariableBlobs) + var hintNames = new Dictionary(); + foreach(var technique in _effectChunk.Techniques) + { + foreach(var assignment in technique.Passes.SelectMany(p => p.Assignments)) + { + if(!_effectChunk.StateBlobLookup.TryGetValue(assignment, out var stateBlob) || stateBlob is null) + { + continue; + } + + if(stateBlob.BlobType is StateBlobType.IndexShader or StateBlobType.Shader) + { + if(!hintNames.TryGetValue(stateBlob, out var existingName)) + { + hintNames.Add(stateBlob, technique.Name); + } + else if(existingName != technique.Name) + { + hintNames[stateBlob] = null; + } + } + } + } + string MakeShaderName(ShaderModel shader) + { + return shader.Type switch + { + ShaderType.Pixel => "PixelShader", + ShaderType.Vertex => "VertexShader", + ShaderType.Expression => "Expression", + _ => "Shader" + } + $"{_shaderNames.Count + 1}"; + } + + foreach(var blob in _effectChunk.VariableBlobs) { - if(blob.IsShader) + if(!blob.IsShader) { - ShaderNames[blob] = $"Shader{shaderCount++}"; + continue; } + _shaderNames[blob] = MakeShaderName(blob.Shader); } - foreach(var blob in EffectChunk.StateBlobs) + foreach(var blob in _effectChunk.StateBlobs) { if(blob.BlobType == StateBlobType.Shader || blob.BlobType == StateBlobType.IndexShader) { - ShaderNames[blob] = $"Shader{shaderCount++}"; + if(!hintNames.TryGetValue(blob, out var techniquePrefix) || string.IsNullOrWhiteSpace(techniquePrefix)) + { + techniquePrefix = string.Empty; + } + else + { + techniquePrefix += "_"; + } + _shaderNames[blob] = techniquePrefix + MakeShaderName(blob.Shader); } } + } + void FindCommonConstantDeclarations() + { + var variableShaders = _effectChunk.VariableBlobs + .Where(x => x.IsShader) + .Select(x => x.Shader); + var blobShaders = _effectChunk.StateBlobs + .Where(x => x.BlobType is StateBlobType.Shader or StateBlobType.IndexShader) + .Select(x => x.Shader); + foreach(var shader in variableShaders.Concat(blobShaders)) + { + var declarations = shader.ConstantTable?.ConstantDeclarations + ?? Enumerable.Empty(); + var decompiled = declarations.Select(c => ConstantTypeWriter.Decompile(c, shader)); + foreach(var declaration in decompiled) + { + if(!CommonConstantDeclarations.TryGetValue(declaration.Name, out var existing)) + { + CommonConstantDeclarations[declaration.Name] = declaration; + } + // this means we don't have common constant declaration + else if(existing.Code != declaration.Code) + { + CommonConstantDeclarations.Remove(declaration.Name); + } + // check if two declarations only differs on register... + else + { + if(!declaration.RegisterAssignments.Any()) + { + continue; + } + // sanity check + if(declaration.RegisterAssignments.Count > 1) + { + throw new InvalidOperationException(); + } + + var registerAssignment = declaration.RegisterAssignments.First(); + if(existing.RegisterAssignments.TryGetValue(registerAssignment.Key, out var existingRegister)) + { + // if register number of the same constant is different + // between two shaders of same shader profile + if(registerAssignment.Value != existingRegister) + { + // then probably there weren't a register assignment at all. + // I don't think there is a way to specify two different registers + // for the same variable with the same shader profile in DX9's effect. + existing.RegisterAssignments.Remove(registerAssignment.Key); + } + } + else + { + existing.RegisterAssignments[registerAssignment.Key] = registerAssignment.Value; + } + } + } + } + + // we should also remove register assignments, + // when there are two different declarations with the same register assignments + var occupiedRegisterAssignments = new Dictionary, DecompiledConstantDeclaration>(); + foreach(var decl in CommonConstantDeclarations.Values) + { + foreach(var assignment in decl.RegisterAssignments) + { + if(occupiedRegisterAssignments.TryGetValue(assignment, out var other)) + { + // remove conflicting register assignments + other.RegisterAssignments.Remove(assignment.Key); + decl.RegisterAssignments.Remove(assignment.Key); + } + else + { + occupiedRegisterAssignments[assignment] = decl; + } + } + } } protected override void Write() { BuildNameLookup(); - foreach(var variable in EffectChunk.Variables) + FindCommonConstantDeclarations(); + foreach(var variable in _effectChunk.Variables) { WriteVariable(variable); } - foreach(var blob in EffectChunk.StateBlobs) + foreach(var blob in _effectChunk.StateBlobs) { if(blob.BlobType == StateBlobType.Shader || blob.BlobType == StateBlobType.IndexShader) @@ -55,50 +179,49 @@ protected override void Write() WriteShader(blob); } } - foreach(var technique in EffectChunk.Techniques) + foreach(var technique in _effectChunk.Techniques) { WriteTechnique(technique); } } - void WriteShader(StateBlob blob) => WriteShader(ShaderNames[blob], blob.Shader); + void WriteShader(StateBlob blob) => WriteShader(_shaderNames[blob], blob.Shader); void WriteShader(string shaderName, ShaderModel shader) { WriteLine($"// {shaderName} {shader.Type}_{shader.MajorVersion}_{shader.MinorVersion} Has PRES {shader.Preshader != null}"); var funcName = shaderName; - var text = ""; + string text; if(shader.Type == ShaderType.Expression) { text = ExpressionHLSLWriter.Decompile(shader, funcName); } else { - text = HlslWriter.Decompile(shader, funcName); - // text = text.Replace("main(", $"{funcName}("); + text = HlslWriter.Decompile(shader, funcName, this); } WriteLine(text); } public string StateBlobToString(Assignment key) { - if(!EffectChunk.StateBlobLookup.ContainsKey(key)) + if(!_effectChunk.StateBlobLookup.ContainsKey(key)) { return $"Key not found"; } - var data = EffectChunk.StateBlobLookup[key]; + var data = _effectChunk.StateBlobLookup[key]; if(data == null) { - return "Blob is NULL"; + return "NULL /* Blob is NULL!!! */"; } if(data.BlobType == StateBlobType.Shader) { if(data.Shader.Type == ShaderType.Expression) { - var funcName = ShaderNames[data]; - return $"{funcName}"; + var funcName = _shaderNames[data]; + return $"{funcName}()"; } else { - var funcName = ShaderNames[data]; - return $"compile {data.Shader.Type.GetDescription()}_{data.Shader.MajorVersion}_{data.Shader.MinorVersion} {funcName}()"; + var funcName = _shaderNames[data]; + return $"compile {data.Shader.Profile} {funcName}()"; } } if(data.BlobType == StateBlobType.Variable) @@ -114,31 +237,35 @@ public string StateBlobToString(Assignment key) } if(data.BlobType == StateBlobType.IndexShader) { - var funcName = ShaderNames[data]; + var funcName = _shaderNames[data]; return $"{data.VariableName}[{funcName}()]"; } throw new ArgumentException(); } public string VariableBlobToString(FX9.Parameter key, int index = 0) { - if(!EffectChunk.VariableBlobLookup.ContainsKey(key)) + if(!_effectChunk.VariableBlobLookup.ContainsKey(key)) { return $"Key not found"; } - var data = EffectChunk.VariableBlobLookup[key][index]; + var data = _effectChunk.VariableBlobLookup[key][index]; if(data == null) { return "Blob is NULL"; } if(data.IsShader) { - var funcName = ShaderNames[data]; + var funcName = _shaderNames[data]; return $"compile {data.Shader.Type.GetDescription()}_{data.Shader.MajorVersion}_{data.Shader.MinorVersion} {funcName}()"; } else if(key.ParameterType == ParameterType.String) { return $"\"{data.Value}\""; } + else if(string.IsNullOrEmpty(data.Value)) + { + return string.Empty; + } else { return $"<{data.Value}>"; @@ -148,23 +275,41 @@ public void WriteVariable(Variable variable) { var param = variable.Parameter; WriteIndent(); - var isShaderArray = param.ParameterClass == ParameterClass.Object && - (param.ParameterType == ParameterType.PixelShader || param.ParameterType == ParameterType.VertexShader); - Dictionary shaderArrayElements = null; + var isShaderArray = param is + { + ParameterClass: ParameterClass.Object, + ParameterType: ParameterType.PixelShader or ParameterType.VertexShader + }; + Dictionary shaderArrayCompileStatements = null; if(isShaderArray) { - shaderArrayElements = new Dictionary(); - var blobs = EffectChunk.VariableBlobLookup[param]; + shaderArrayCompileStatements = new Dictionary(); + var blobs = _effectChunk.VariableBlobLookup[param]; var index = 0; foreach(var blob in blobs) { var name = $"{variable.Parameter.Name}_Shader_{index}"; - shaderArrayElements.Add(blob.Index, name); - WriteShader(name, blob.Shader); + var shader = blob.Shader; + var compileStatement = $"compile {shader.Type.GetDescription()}_{shader.MajorVersion}_{shader.MinorVersion} {name}()"; + shaderArrayCompileStatements.Add(blob.Index, compileStatement); + WriteShader(name, shader); ++index; } } - Write(param.GetDecleration()); + if(CommonConstantDeclarations.TryGetValue(variable.Parameter.Name, out var decompiled)) + { + var semantic = string.IsNullOrEmpty(param.Semantic) + ? string.Empty + : $" : {param.Semantic}"; + Write($"{decompiled.Code}{semantic}{decompiled.RegisterAssignmentString}"); + } + // shader's constant declaration might differ from the effect variable's parameter declaration + // in that case, we should prefer shader's one. + // So we write parameter's declaration only if the variable isn't inside the common constant declaration. + else + { + Write(param.GetDeclaration()); + } if(variable.Annotations.Count > 0) { Write(" "); @@ -192,7 +337,7 @@ public void WriteVariable(Variable variable) if(assignment.Type == StateType.Texture) { var data = StateBlobToString(assignment); - WriteLine("{0} = <{1}>; // {2}", assignment.Type, data, assignment.Value[0].UInt); + WriteLine("{0} = {1}; // {2}", assignment.Type, data, assignment.Value[0].UInt); } else { @@ -233,7 +378,7 @@ public void WriteVariable(Variable variable) foreach(var idx in variable.DefaultValue) { WriteIndent(); - WriteLine("{0}, // {1}", shaderArrayElements[idx.UInt], idx.UInt); + WriteLine("{0}, // {1}", shaderArrayCompileStatements[idx.UInt], idx.UInt); } Indent--; WriteLine("};"); @@ -249,7 +394,7 @@ public void WriteVariable(Variable variable) { WriteLine(" = {0}; // {1}", data, variable.DefaultValue[0].UInt); } - } + } } else if(variable.DefaultValue.All(d => d.UInt == 0)) { @@ -269,6 +414,10 @@ public void WriteAnnotations(List annotations) { var annotation = annotations[i]; var value = string.Join(", ", annotation.Value); + if(annotation.Value.Count > 1) + { + value = string.Format("{0}({1})", annotation.Parameter.GetTypeName(), value); + } if(annotation.Parameter.ParameterType.HasVariableBlob()) { Write("{0} {1} = {2};", @@ -334,20 +483,20 @@ public void WriteAssignment(Assignment assignment) index = string.Format("[{0}]", assignment.ArrayIndex); } string value; - if(assignment.Value.Count > 1) + if(_effectChunk.StateBlobLookup.ContainsKey(assignment)) { - value = string.Format("{{ {0} }}", string.Join(", ", assignment.Value)); + value = StateBlobToString(assignment); } - else if(EffectChunk.StateBlobLookup.ContainsKey(assignment)) + else if(assignment.Value.Count > 1) { - value = StateBlobToString(assignment); + value = string.Format("{{ {0} }}", string.Join(", ", assignment.Value)); } else { value = assignment.Value[0].ToString(); } Write("{0}{1} = {2};", assignment.Type.ToString(), index, value); - if(EffectChunk.StateBlobLookup.ContainsKey(assignment)) + if(_effectChunk.StateBlobLookup.ContainsKey(assignment)) { Write(" // {0}", assignment.Value[0].UInt); } diff --git a/src/DXDecompiler/DX9Shader/Decompiler/ExpressionWriter.cs b/src/DXDecompiler/DX9Shader/Decompiler/ExpressionWriter.cs index 4e0e85b0..a2544464 100644 --- a/src/DXDecompiler/DX9Shader/Decompiler/ExpressionWriter.cs +++ b/src/DXDecompiler/DX9Shader/Decompiler/ExpressionWriter.cs @@ -1,22 +1,16 @@ -using DXDecompiler.DX9Shader.Bytecode; -using DXDecompiler.DX9Shader.Bytecode.Ctab; -using DXDecompiler.DX9Shader.Bytecode.Fxlvm; +using DXDecompiler.DX9Shader.Bytecode.Fxlvm; +using System; using System.Linq; namespace DXDecompiler.DX9Shader.Decompiler { - class ExpressionHLSLWriter : DecompileWriter + class ExpressionHLSLWriter : FxlcHlslWriter { - ShaderModel Shader; - ConstantTable Ctab; - CliToken Cli; - string ExpressionName; - public ExpressionHLSLWriter(ShaderModel shader, string expressionName) + string ExpressionName { get; } + + public ExpressionHLSLWriter(ShaderModel shader, string expressionName) : base(shader) { - Shader = shader; ExpressionName = expressionName; - Ctab = shader.ConstantTable; - Cli = shader.Cli; } public static string Decompile(ShaderModel shader, string expressionName = "Expression") { @@ -25,145 +19,63 @@ public static string Decompile(ShaderModel shader, string expressionName = "Expr } protected override void Write() { - WriteLine($"float4 {ExpressionName}()"); - WriteLine("{"); - Indent++; - WriteIndent(); - WriteLine("float4 expr;"); - foreach(var token in Shader.Fxlc.Tokens) + var (rows, columns) = GetOutputDimensions(); + string returnType; + if(rows == 1) { - Write(token); + returnType = columns == 1 ? "float" : $"float{columns}"; } - WriteIndent(); - WriteLine("return expr;"); - Indent--; - WriteLine("}"); - - if(Shader.Preshader != null) + else { - Write("Have Pres"); + returnType = $"float{rows}x{columns}"; } - } - void Write(FxlcToken token) - { + WriteLine($"{returnType} {ExpressionName}()"); + WriteLine("{"); + Indent++; + + WriteTemporaries(); + var destinationVariables = string.Join(", ", Enumerable.Range(0, rows).Select(i => $"expr{i}")); WriteIndent(); - WriteLine($"// {token.ToString(Shader.ConstantTable, Shader.Cli)}"); - switch(token.Opcode) - { - case Bytecode.Fxlvm.FxlcOpcode.Mov: - WriteIndent(); - WriteLine("{0} = {1};", - token.Operands[0].FormatOperand(Ctab, Cli), - token.Operands[1].FormatOperand(Ctab, Cli)); - break; - case Bytecode.Fxlvm.FxlcOpcode.Neg: - WriteIndent(); - WriteLine("{0} = -{1};", - token.Operands[0].FormatOperand(Ctab, Cli), - token.Operands[1].FormatOperand(Ctab, Cli)); - break; - case Bytecode.Fxlvm.FxlcOpcode.Frc: - WriteFunction("frac", token); - break; - case Bytecode.Fxlvm.FxlcOpcode.Exp: - WriteFunction("exp", token); - break; - case Bytecode.Fxlvm.FxlcOpcode.Log: - WriteFunction("log", token); - break; - case Bytecode.Fxlvm.FxlcOpcode.Rsq: - WriteFunction("rsq", token); - break; - case Bytecode.Fxlvm.FxlcOpcode.Sin: - WriteFunction("sin", token); - break; - case Bytecode.Fxlvm.FxlcOpcode.Cos: - WriteFunction("cos", token); - break; - case Bytecode.Fxlvm.FxlcOpcode.Asin: - WriteFunction("asin", token); - break; - case Bytecode.Fxlvm.FxlcOpcode.Acos: - WriteFunction("acos", token); - break; - case Bytecode.Fxlvm.FxlcOpcode.Atan: - WriteFunction("atam", token); - break; - case Bytecode.Fxlvm.FxlcOpcode.Atan2: - WriteFunction("atan2", token); - break; - case Bytecode.Fxlvm.FxlcOpcode.Sqrt: - WriteFunction("sqrt", token); - break; - case Bytecode.Fxlvm.FxlcOpcode.Ineg: - WriteFunction("~int", token); - break; - case Bytecode.Fxlvm.FxlcOpcode.Imax: - WriteFunction("(int)max(", token); - break; - case Bytecode.Fxlvm.FxlcOpcode.Not: - WriteFunction("!", token); - break; - case Bytecode.Fxlvm.FxlcOpcode.Utof: - WriteFunction("utof", token); - break; - case Bytecode.Fxlvm.FxlcOpcode.Ftoi: - WriteFunction("ftoi", token); - break; - case Bytecode.Fxlvm.FxlcOpcode.Ftou: - WriteFunction("ftou", token); - break; - case Bytecode.Fxlvm.FxlcOpcode.Btoi: - WriteFunction("btoi", token); - break; - case Bytecode.Fxlvm.FxlcOpcode.Round: - WriteFunction("round", token); - break; - case Bytecode.Fxlvm.FxlcOpcode.Floor: - WriteFunction("floor", token); - break; - case Bytecode.Fxlvm.FxlcOpcode.Ceil: - WriteFunction("ceil", token); - break; - case Bytecode.Fxlvm.FxlcOpcode.Min: - WriteFunction("min", token); - break; - case Bytecode.Fxlvm.FxlcOpcode.Max: - WriteFunction("max", token); - break; - case Bytecode.Fxlvm.FxlcOpcode.Add: - WriteInfix("+", token); - break; - case Bytecode.Fxlvm.FxlcOpcode.Mul: - WriteInfix("*", token); - break; + WriteLine($"float{columns} {destinationVariables};"); + WriteInstructions(); - case Bytecode.Fxlvm.FxlcOpcode.Lt: - WriteInfix("<", token); - break; - } - } - void WriteInfix(string op, FxlcToken token) - { WriteIndent(); - WriteLine("{0} = {1} {2} {3};", - token.Operands[0].FormatOperand(Ctab, Cli), - token.Operands[1].FormatOperand(Ctab, Cli), - op, - token.Operands[2].FormatOperand(Ctab, Cli)); + if(rows == 1) + { + WriteLine("return expr0;"); + } + else + { + + WriteLine($"return {returnType}({destinationVariables});"); + } + + Indent--; + WriteLine("}"); } - void WriteFunction(string func, FxlcToken token) + private (int Rows, int Columns) GetOutputDimensions() { - WriteIndent(); - var operands = token.Operands - .Skip(1) - .Select(o => o.FormatOperand(Ctab, Cli)); - WriteLine("{0} = {1}({2});", - token.Operands[0].FormatOperand(Ctab, Cli), - func, - string.Join(", ", operands)); + int registerNumber = 0; + int components = 0; + foreach(var token in Shader.Fxlc.Tokens) + { + var destination = token.Operands[0]; + if(destination.IsArray != 0 && destination.ArrayType == FxlcOperandType.Expr) + { + throw new NotImplementedException(); + } + if(destination.OpType != FxlcOperandType.Expr) + { + continue; + } + registerNumber = Math.Max(registerNumber, (int)destination.OpIndex / 4); + var extraComponents = Math.Max(0, (int)destination.ComponentCount - 1); + var lastIndex = Math.Min(4, (int)destination.OpIndex % 4 + extraComponents); + components = Math.Max(components, lastIndex); + } + return (registerNumber + 1, components + 1); } } } diff --git a/src/DXDecompiler/DX9Shader/Decompiler/FxlcHlslWriter.cs b/src/DXDecompiler/DX9Shader/Decompiler/FxlcHlslWriter.cs new file mode 100644 index 00000000..eaa0efbf --- /dev/null +++ b/src/DXDecompiler/DX9Shader/Decompiler/FxlcHlslWriter.cs @@ -0,0 +1,197 @@ +using DXDecompiler.DX9Shader.Bytecode; +using DXDecompiler.DX9Shader.Bytecode.Ctab; +using DXDecompiler.DX9Shader.Bytecode.Fxlvm; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace DXDecompiler.DX9Shader.Decompiler +{ + class FxlcHlslWriter : DecompileWriter + { + protected ShaderModel Shader { get; } + protected ConstantTable Ctab { get; } + protected CliToken Cli { get; } + + public FxlcHlslWriter(ShaderModel shader) + { + Shader = shader; + Ctab = shader.ConstantTable; + Cli = shader.Cli; + } + + protected void WriteTemporaries() + { + var temporaryRegisters = new SortedSet(); + foreach(var operands in Shader.Fxlc.Tokens.SelectMany(t => t.Operands)) + { + if(operands.IsArray != 0) + { + if(operands.ArrayType == FxlcOperandType.Temp) + { + // will this ever happen? + throw new NotImplementedException(); + } + } + if(operands.OpType == FxlcOperandType.Temp) + { + temporaryRegisters.Add(operands.OpIndex / 4); + } + } + foreach(var tempIndex in temporaryRegisters) + { + WriteIndent(); + WriteLine($"float4 temp{tempIndex};"); + } + } + + protected void WriteInstructions(HashSet ctabOverride = null) + { + foreach(var token in Shader.Fxlc.Tokens) + { + Write(token, ctabOverride); + } + } + + void Write(FxlcToken token, HashSet ctabOverride = null) + { + WriteIndent(); + WriteLine($"// {token.ToString(Shader.Cli)}"); + switch(token.Opcode) + { + default: + throw new NotImplementedException(token.Opcode.ToString()); + case FxlcOpcode.Rcp: + WriteFunction("1.0f / ", token, ctabOverride); + break; + case FxlcOpcode.Mov: + WriteAssignment(token, ctabOverride, "{0}", token.Operands[1].FormatOperand(Cli, Ctab, ctabOverride)); + break; + case FxlcOpcode.Neg: + WriteAssignment(token, ctabOverride, "-{0}", token.Operands[1].FormatOperand(Cli, Ctab, ctabOverride)); + break; + case FxlcOpcode.Frc: + WriteFunction("frac", token, ctabOverride); + break; + case FxlcOpcode.Exp: + WriteFunction("exp", token, ctabOverride); + break; + case FxlcOpcode.Log: + WriteFunction("log", token, ctabOverride); + break; + case FxlcOpcode.Rsq: + WriteFunction("1.0f / sqrt", token, ctabOverride); + break; + case FxlcOpcode.Sin: + WriteFunction("sin", token, ctabOverride); + break; + case FxlcOpcode.Cos: + WriteFunction("cos", token, ctabOverride); + break; + case FxlcOpcode.Asin: + WriteFunction("asin", token, ctabOverride); + break; + case FxlcOpcode.Acos: + WriteFunction("acos", token, ctabOverride); + break; + case FxlcOpcode.Atan: + WriteFunction("atan", token, ctabOverride); + break; + case FxlcOpcode.Atan2: + WriteFunction("atan2", token, ctabOverride); + break; + case FxlcOpcode.Sqrt: + WriteFunction("sqrt", token, ctabOverride); + break; + case FxlcOpcode.Ineg: + WriteFunction("~int", token, ctabOverride); + break; + case FxlcOpcode.Imax: + WriteFunction("(int)max", token, ctabOverride); + break; + case FxlcOpcode.Not: + WriteFunction("!", token, ctabOverride); + break; + case FxlcOpcode.Utof: + WriteFunction("utof", token, ctabOverride); + break; + case FxlcOpcode.Ftoi: + WriteFunction("ftoi", token, ctabOverride); + break; + case FxlcOpcode.Ftou: + WriteFunction("ftou", token, ctabOverride); + break; + case FxlcOpcode.Btoi: + WriteFunction("btoi", token, ctabOverride); + break; + case FxlcOpcode.Round: + WriteFunction("round", token, ctabOverride); + break; + case FxlcOpcode.Floor: + WriteFunction("floor", token, ctabOverride); + break; + case FxlcOpcode.Ceil: + WriteFunction("ceil", token, ctabOverride); + break; + case FxlcOpcode.Min: + WriteFunction("min", token, ctabOverride); + break; + case FxlcOpcode.Max: + WriteFunction("max", token, ctabOverride); + break; + case FxlcOpcode.Dot: + WriteFunction("dot", token, ctabOverride); + break; + case FxlcOpcode.Add: + WriteInfix("+", token, ctabOverride); + break; + case FxlcOpcode.Mul: + WriteInfix("*", token, ctabOverride); + break; + case FxlcOpcode.Lt: + WriteInfix("<", token, ctabOverride); + break; + case FxlcOpcode.Ge: + WriteInfix(">=", token, ctabOverride); + break; + case FxlcOpcode.Cmp: + WriteAssignment(token, ctabOverride, "({0} >= 0 ? {1} : {2})", + token.Operands.Skip(1).Select(o => o.FormatOperand(Cli, Ctab, ctabOverride)).ToArray()); + break; + } + } + void WriteAssignment(FxlcToken token, HashSet ctabOverride, string format, params string[] args) + { + var destination = token.Operands[0]; + if(destination.ArrayIndex % 4 != 0) + { + // I'm not sure if write masks apply to preshaders as well... + throw new NotImplementedException(); + } + WriteIndent(); + WriteLine("{0} = {1};", destination.FormatOperand(Cli, Ctab, ctabOverride), string.Format(format, args)); + if(destination.IsArray != 0) + { + throw new NotImplementedException(); + } + if(destination.OpType == FxlcOperandType.Expr) + { + ctabOverride?.Add(destination.OpIndex / 4); + } + } + void WriteInfix(string op, FxlcToken token, HashSet ctabOverride) + { + WriteAssignment(token, ctabOverride, "{0} {1} {2}", + token.Operands[1].FormatOperand(Cli, Ctab, ctabOverride), + op, + token.Operands[2].FormatOperand(Cli, Ctab, ctabOverride)); + } + void WriteFunction(string func, FxlcToken token, HashSet ctabOverride) + { + var operands = token.Operands + .Skip(1) + .Select(o => o.FormatOperand(Cli, Ctab, ctabOverride)); + WriteAssignment(token, ctabOverride, "{0}({1})", func, string.Join(", ", operands)); + } + } +} diff --git a/src/DXDecompiler/DX9Shader/Decompiler/HlslWriter.cs b/src/DXDecompiler/DX9Shader/Decompiler/HlslWriter.cs index afad8dfb..850998d4 100644 --- a/src/DXDecompiler/DX9Shader/Decompiler/HlslWriter.cs +++ b/src/DXDecompiler/DX9Shader/Decompiler/HlslWriter.cs @@ -1,4 +1,5 @@ -using DXDecompiler.DX9Shader.Bytecode.Ctab; +using DXDecompiler.DX9Shader.Bytecode.Ctab; +using DXDecompiler.DX9Shader.Decompiler; using DXDecompiler.Util; using System; using System.Collections.Generic; @@ -8,17 +9,47 @@ namespace DXDecompiler.DX9Shader { public class HlslWriter : DecompileWriter { + private class SourceOperand + { + public string Body { get; set; } // normally, register / constant name, or type name if Literals are not null + public string[] Literals { get; set; } // either null, or literal values + public string Swizzle { get; set; } // either empty, or a swizzle with leading dot. Empty if Literals are not null. + public string Modifier { get; set; } // should be used with string.Format to format the body. Should be "{0}" if Literals are not null. + public ParameterType? SamplerType { get; set; } // not null if it's a sampler + + public override string ToString() + { + var body = Body; + if(Literals is not null) + { + body += string.Format("{0}({1})", Literals.Length, string.Join(", ", Literals)); + } + body = string.Format(Modifier, body); + if(body.All(char.IsDigit)) + { + body = $"({body})"; + } + return body + Swizzle; + } + } + private readonly ShaderModel _shader; + private readonly AsmWriter _disassember; private readonly bool _doAstAnalysis; private int _iterationDepth = 0; - public RegisterState _registers; + private EffectHLSLWriter _effectWriter; + private RegisterState _registers; string _entryPoint; public HlslWriter(ShaderModel shader, bool doAstAnalysis = false, string entryPoint = null) { _shader = shader; _doAstAnalysis = doAstAnalysis; + if(!_doAstAnalysis) + { + _disassember = new(shader); + } if(string.IsNullOrEmpty(entryPoint)) { _entryPoint = $"{_shader.Type}Main"; @@ -35,59 +66,89 @@ public static string Decompile(byte[] bytecode, string entryPoint = null) var shaderModel = ShaderReader.ReadShader(bytecode); return Decompile(shaderModel); } - public static string Decompile(ShaderModel shaderModel, string entryPoint = null) + public static string Decompile(ShaderModel shaderModel, string entryPoint = null, EffectHLSLWriter effect = null) { if(shaderModel.Type == ShaderType.Effect) { return EffectHLSLWriter.Decompile(shaderModel.EffectChunk); } - var hlslWriter = new HlslWriter(shaderModel, false, entryPoint); + var hlslWriter = new HlslWriter(shaderModel, false, entryPoint) + { + _effectWriter = effect + }; return hlslWriter.Decompile(); } - private string GetDestinationName(InstructionToken instruction) + private string GetDestinationName(InstructionToken instruction, out string writeMaskName) { - return _registers.GetDestinationName(instruction); + return _registers.GetDestinationName(instruction, out writeMaskName); } - private string GetSourceName(InstructionToken instruction, int srcIndex) + private string GetDestinationNameWithWriteMask(InstructionToken instruction) { - return _registers.GetSourceName(instruction, srcIndex); + var destinationName = GetDestinationName(instruction, out var writeMask); + return destinationName + writeMask; } - private static string GetConstantTypeName(ConstantType type) + private SourceOperand GetSourceName(InstructionToken instruction, int srcIndex, bool isLogicalIndex = true) { - switch(type.ParameterClass) - { - case ParameterClass.Scalar: - return type.ParameterType.GetDescription(); - case ParameterClass.Vector: - return type.ParameterType.GetDescription() + type.Columns; - case ParameterClass.Struct: - return "struct"; - case ParameterClass.MatrixColumns: - return $"column_major {type.ParameterType.GetDescription()}{type.Rows}x{type.Columns}"; - case ParameterClass.MatrixRows: - return $"row_major {type.ParameterType.GetDescription()}{type.Rows}x{type.Columns}"; - case ParameterClass.Object: - switch(type.ParameterType) + int dataIndex; + if(isLogicalIndex) + { + // compute the actual data index, which might be different from logical index + // because of relative addressing mode. + + // TODO: Handle relative addressing mode in a better way, + // by using `InstructionToken.Operands`: + // https://github.com/spacehamster/DXDecompiler/pull/6#issuecomment-782958769 + + // if instruction has destination, then source starts at the index 1 + // here we assume destination won't have relative addressing, + // so we assume destination will only occupy 1 slot, + // that is, the start index for sources will be 1 if instruction.HasDestination is true. + var begin = instruction.HasDestination ? 1 : 0; + dataIndex = begin; + while(srcIndex > begin) + { + if(instruction.IsRelativeAddressMode(dataIndex)) { - case ParameterType.Sampler1D: - case ParameterType.Sampler2D: - case ParameterType.Sampler3D: - case ParameterType.SamplerCube: - return "sampler"; - default: - throw new NotImplementedException(); + ++dataIndex; } + ++dataIndex; + --srcIndex; + } + } + else + { + dataIndex = srcIndex; + } + + ParameterType? samplerType = null; + var registerNumber = instruction.GetParamRegisterNumber(dataIndex); + var registerType = instruction.GetParamRegisterType(dataIndex); + if(registerType == RegisterType.Sampler) + { + var decl = _registers.FindConstant(RegisterSet.Sampler, registerNumber); + var type = decl.GetRegisterTypeByOffset(registerNumber - decl.RegisterIndex); + samplerType = type.Type.ParameterType; } - throw new NotImplementedException(); + + + var body = _registers.GetSourceName(instruction, dataIndex, out var swizzle, out var modifier, out var literals); + return new SourceOperand + { + Body = body, + Literals = literals, + Swizzle = swizzle, + Modifier = modifier, + SamplerType = samplerType + }; } private void WriteInstruction(InstructionToken instruction) { WriteIndent(); - WriteLine($"// {instruction}"); + WriteLine($"// {_disassember?.Disassemble(instruction).Trim()}"); switch(instruction.Opcode) { case Opcode.Def: @@ -115,38 +176,134 @@ private void WriteInstruction(InstructionToken instruction) return; } WriteIndent(); + + void WriteAssignment(string sourceFormat, params SourceOperand[] args) + { + var destination = GetDestinationName(instruction, out var writeMask); + var destinationModifier = instruction.GetDestinationResultModifier() switch + { + ResultModifier.None => "{0} = {1};", + ResultModifier.Saturate => "{0} = saturate({1});", + ResultModifier.PartialPrecision => $"{{0}} = /* not implemented _pp modifier */ {{1}};", + object unknown => throw new NotImplementedException($"{unknown}") + }; + var sourceResult = string.Format(sourceFormat, args); + + var swizzleSizes = args.Select(x => x.Swizzle.StartsWith(".") ? x.Swizzle.Trim('.').Length : -1); + var returnsScalar = instruction.Opcode.ReturnsScalar() || swizzleSizes.All(x => x == 1); + + if(writeMask.Length > 0) + { + destination += writeMask; + if(returnsScalar) + { + // do nothing, don't need to append write mask as swizzle + } + // if the instruction is parallel then we are safe to "edit" source swizzles + else if(instruction.Opcode.IsParallel(_shader)) + { + foreach(var arg in args) + { + const string xyzw = ".xyzw"; + + if(arg.Literals is not null) + { + arg.Literals = arg.Literals + .Where((v, i) => writeMask.Contains(xyzw[i + 1])) + .ToArray(); + continue; + } + + var trimmedSwizzle = "."; + if(string.IsNullOrEmpty(arg.Swizzle)) + { + arg.Swizzle = xyzw; + } + while(arg.Swizzle.Length <= 4) + { + arg.Swizzle += arg.Swizzle.Last(); + } + for(var i = 1; i <= 4; ++i) + { + if(writeMask.Contains(xyzw[i])) + { + trimmedSwizzle += arg.Swizzle[i]; + } + } + arg.Swizzle = trimmedSwizzle; + } + sourceResult = string.Format(sourceFormat, args); + } + // if we cannot "edit" the swizzles, we need to apply write masks on the source result + else + { + if(sourceResult.Last() != ')') + { + sourceResult = $"({sourceResult})"; + } + sourceResult += writeMask; + } + } + WriteLine(destinationModifier, destination, sourceResult); + } + + void WriteTextureAssignment(string postFix, SourceOperand sampler, SourceOperand uv, int? dimension, params SourceOperand[] others) + { + var (operation, defaultDimension) = sampler.SamplerType switch + { + ParameterType.Sampler1D => ("tex1D", 1), + ParameterType.Sampler2D => ("tex2D", 2), + ParameterType.Sampler3D => ("tex3D", 3), + ParameterType.SamplerCube => ("texCUBE", 3), + ParameterType.Sampler => ("texUnknown", 4), + _ => throw new InvalidOperationException(sampler.SamplerType.ToString()) + }; + dimension ??= defaultDimension; + var args = new SourceOperand[others.Length + 2]; + var uvSwizzle = uv.Swizzle.TrimStart('.'); + if(uvSwizzle.Length == 0) + { + uvSwizzle = "xyzw"; + } + if(uvSwizzle.Length > dimension) + { + uv.Swizzle = "." + uvSwizzle.Substring(0, dimension.Value); + } + args[0] = sampler; + args[1] = uv; + others.CopyTo(args, 2); + var format = string.Join(", ", args.Select((_, i) => $"{{{i}}}")); + WriteAssignment($"{operation}{postFix}({format})", args); + } + switch(instruction.Opcode) { case Opcode.Abs: - WriteLine("{0} = abs({1});", GetDestinationName(instruction), - GetSourceName(instruction, 1)); + WriteAssignment("abs({0})", GetSourceName(instruction, 1)); break; case Opcode.Add: - WriteLine("{0} = {1} + {2};", GetDestinationName(instruction), - GetSourceName(instruction, 1), GetSourceName(instruction, 2)); + WriteAssignment("{0} + {1}", GetSourceName(instruction, 1), GetSourceName(instruction, 2)); break; case Opcode.Cmp: // TODO: should be per-component - WriteLine("{0} = ({1} >= 0) ? {2} : {3};", GetDestinationName(instruction), + WriteAssignment("({0} >= 0) ? {1} : {2}", GetSourceName(instruction, 1), GetSourceName(instruction, 2), GetSourceName(instruction, 3)); break; case Opcode.DP2Add: - WriteLine("{0} = dot({1}, {2}) + {3};", GetDestinationName(instruction), + WriteAssignment("dot({0}, {1}) + {2}", GetSourceName(instruction, 1), GetSourceName(instruction, 2), GetSourceName(instruction, 3)); break; case Opcode.Dp3: - WriteLine("{0} = dot({1}, {2});", GetDestinationName(instruction), - GetSourceName(instruction, 1), GetSourceName(instruction, 2)); + WriteAssignment("dot({0}, {1})", GetSourceName(instruction, 1), GetSourceName(instruction, 2)); break; case Opcode.Dp4: - WriteLine("{0} = dot({1}, {2});", GetDestinationName(instruction), - GetSourceName(instruction, 1), GetSourceName(instruction, 2)); + WriteAssignment("dot({0}, {1})", GetSourceName(instruction, 1), GetSourceName(instruction, 2)); break; case Opcode.Exp: - WriteLine("{0} = exp2({1});", GetDestinationName(instruction), GetSourceName(instruction, 1)); + WriteAssignment("exp2({0})", GetSourceName(instruction, 1)); break; case Opcode.Frc: - WriteLine("{0} = frac({1});", GetDestinationName(instruction), GetSourceName(instruction, 1)); + WriteAssignment("frac({0})", GetSourceName(instruction, 1)); break; case Opcode.If: WriteLine("if ({0}) {{", GetSourceName(instruction, 0)); @@ -200,46 +357,71 @@ private void WriteInstruction(InstructionToken instruction) Indent++; break; case Opcode.Log: - WriteLine("{0} = log2({1});", GetDestinationName(instruction), GetSourceName(instruction, 1)); + WriteAssignment("log2({0})", GetSourceName(instruction, 1)); break; case Opcode.Lrp: - WriteLine("{0} = lerp({2}, {3}, {1});", GetDestinationName(instruction), + WriteAssignment("lerp({2}, {1}, {0})", GetSourceName(instruction, 1), GetSourceName(instruction, 2), GetSourceName(instruction, 3)); break; case Opcode.Mad: - WriteLine("{0} = {1} * {2} + {3};", GetDestinationName(instruction), + WriteAssignment("{0} * {1} + {2}", GetSourceName(instruction, 1), GetSourceName(instruction, 2), GetSourceName(instruction, 3)); break; case Opcode.Max: - WriteLine("{0} = max({1}, {2});", GetDestinationName(instruction), - GetSourceName(instruction, 1), GetSourceName(instruction, 2)); + WriteAssignment("max({0}, {1})", GetSourceName(instruction, 1), GetSourceName(instruction, 2)); break; case Opcode.Min: - WriteLine("{0} = min({1}, {2});", GetDestinationName(instruction), - GetSourceName(instruction, 1), GetSourceName(instruction, 2)); + WriteAssignment("min({0}, {1})", GetSourceName(instruction, 1), GetSourceName(instruction, 2)); break; case Opcode.Mov: - WriteLine("{0} = {1};", GetDestinationName(instruction), GetSourceName(instruction, 1)); + WriteAssignment("{0}", GetSourceName(instruction, 1)); break; case Opcode.MovA: - WriteLine("{0} = {1};", GetDestinationName(instruction), GetSourceName(instruction, 1)); + WriteAssignment("{0}", GetSourceName(instruction, 1)); break; case Opcode.Mul: - WriteLine("{0} = {1} * {2};", GetDestinationName(instruction), - GetSourceName(instruction, 1), GetSourceName(instruction, 2)); + WriteAssignment("{0} * {1}", GetSourceName(instruction, 1), GetSourceName(instruction, 2)); break; case Opcode.Nrm: - WriteLine("{0} = normalize({1});", GetDestinationName(instruction), GetSourceName(instruction, 1)); - break; + { + // the nrm opcode actually only works on the 3D vector + var operand = GetSourceName(instruction, 1); + if(instruction.GetDestinationMaskedLength() < 4) + { + var swizzle = operand.Swizzle.TrimStart('.'); + switch(swizzle.Length) + { + case 0: + case 4: + WriteAssignment("normalize({0}.xyz)", operand); + break; + case 1: + // let it reach 3 dimensions + operand.Swizzle += swizzle; + operand.Swizzle += swizzle; + goto case 3; + case 3: + WriteAssignment("normalize({0})", operand); + break; + default: + WriteAssignment("({0} / length(float3({0}))", operand); + break; + } + } + else + { + WriteAssignment("({0} / length(float3({0}))", operand); + } + break; + } case Opcode.Pow: - WriteLine("{0} = pow({1}, {2});", GetDestinationName(instruction), - GetSourceName(instruction, 1), GetSourceName(instruction, 2)); + WriteAssignment("pow({0}, {1})", GetSourceName(instruction, 1), GetSourceName(instruction, 2)); break; case Opcode.Rcp: - WriteLine("{0} = 1 / {1};", GetDestinationName(instruction), GetSourceName(instruction, 1)); + WriteAssignment("1.0f / {0}", GetSourceName(instruction, 1)); break; case Opcode.Rsq: - WriteLine("{0} = 1 / sqrt({1});", GetDestinationName(instruction), GetSourceName(instruction, 1)); + WriteAssignment("1 / sqrt({0})", GetSourceName(instruction, 1)); break; case Opcode.Sge: if(instruction.GetSourceModifier(1) == SourceModifier.AbsAndNegate && @@ -247,40 +429,79 @@ private void WriteInstruction(InstructionToken instruction) instruction.GetParamRegisterName(1) + instruction.GetSourceSwizzleName(1) == instruction.GetParamRegisterName(2) + instruction.GetSourceSwizzleName(2)) { - WriteLine("{0} = ({1} == 0) ? 1 : 0;", GetDestinationName(instruction), - instruction.GetParamRegisterName(1) + instruction.GetSourceSwizzleName(1)); + WriteAssignment("({0} == 0) ? 1 : 0", new SourceOperand + { + Body = instruction.GetParamRegisterName(1), + Swizzle = instruction.GetSourceSwizzleName(1), + Modifier = "{0}" + }); } else { - WriteLine("{0} = ({1} >= {2}) ? 1 : 0;", GetDestinationName(instruction), GetSourceName(instruction, 1), + WriteAssignment("({0} >= {1}) ? 1 : 0", GetSourceName(instruction, 1), GetSourceName(instruction, 2)); } break; case Opcode.Slt: - WriteLine("{0} = ({1} < {2}) ? 1 : 0;", GetDestinationName(instruction), GetSourceName(instruction, 1), - GetSourceName(instruction, 2)); + WriteAssignment("({0} < {1}) ? 1 : 0", GetSourceName(instruction, 1), GetSourceName(instruction, 2)); break; case Opcode.SinCos: - WriteLine("sincos({1}, {0}, {0});", GetDestinationName(instruction), GetSourceName(instruction, 1)); + { + var writeMask = instruction.GetDestinationWriteMask(); + var values = new List(2); + if(writeMask.HasFlag(ComponentFlags.X)) + { + values.Add("cos({0})"); + } + if(writeMask.HasFlag(ComponentFlags.Y)) + { + values.Add("sin({0})"); + } + var source = string.Join(", ", values); + source = values.Count > 1 ? $"float{values.Count}({source})" : source; + WriteAssignment(source, GetSourceName(instruction, 1)); + } break; case Opcode.Sub: - WriteLine("{0} = {1} - {2};", GetDestinationName(instruction), - GetSourceName(instruction, 1), GetSourceName(instruction, 2)); + WriteAssignment("{0} - {1}", GetSourceName(instruction, 1), GetSourceName(instruction, 2)); break; case Opcode.Tex: - if((_shader.MajorVersion == 1 && _shader.MinorVersion >= 4) || (_shader.MajorVersion > 1)) + if(_shader.MajorVersion > 1) { - WriteLine("{0} = tex2D({2}, {1});", GetDestinationName(instruction), - GetSourceName(instruction, 1), GetSourceName(instruction, 2)); + WriteTextureAssignment(string.Empty, GetSourceName(instruction, 2), GetSourceName(instruction, 1), null); + } + // shader model 1 + else if(_shader.MinorVersion >= 4) + { + throw new NotImplementedException("texld in ps_1_4 not implemented yet"); } else { - WriteLine("{0} = tex2D();", GetDestinationName(instruction)); + var uvName = GetDestinationName(instruction, out var writeMask); + var uvOperand = new SourceOperand + { + Body = uvName, + Swizzle = string.Empty, + Modifier = "{0}" + }; + + var registerNumber = instruction.GetParamRegisterNumber(0); + var samplerDecl = _registers.FindConstant(RegisterSet.Sampler, instruction.GetParamRegisterNumber(0)); + var samplerName = samplerDecl.GetConstantNameByRegisterNumber(registerNumber, null); + var samplerType = samplerDecl.GetRegisterTypeByOffset(registerNumber - samplerDecl.RegisterIndex).Type.ParameterType; + var samplerOperand = new SourceOperand + { + Body = samplerName, + Swizzle = string.Empty, + Modifier = "{0}", + SamplerType = samplerType + }; + + WriteTextureAssignment(string.Empty, samplerOperand, uvOperand, null); } break; case Opcode.TexLDL: - WriteLine("{0} = tex2Dlod({2}, {1});", GetDestinationName(instruction), - GetSourceName(instruction, 1), GetSourceName(instruction, 2)); + WriteTextureAssignment("lod", GetSourceName(instruction, 2), GetSourceName(instruction, 1), 4); break; case Opcode.Comment: { @@ -296,7 +517,20 @@ private void WriteInstruction(InstructionToken instruction) Indent++; break; case Opcode.TexKill: - WriteLine("clip({0});", GetDestinationName(instruction)); + if(instruction.GetDestinationResultModifier() is not ResultModifier.None) + { + throw new NotImplementedException("Result modifier in texkill"); + } + WriteLine("clip({0});", GetDestinationNameWithWriteMask(instruction)); + break; + case Opcode.DSX: + WriteAssignment("ddx({0})", GetSourceName(instruction, 1)); + break; + case Opcode.DSY: + WriteAssignment("ddy({0})", GetSourceName(instruction, 1)); + break; + case Opcode.Lit: + WriteAssignment("lit({0}.x, {0}.y, {0}.w)", GetSourceName(instruction, 1)); break; default: throw new NotImplementedException(instruction.Opcode.ToString()); @@ -311,14 +545,14 @@ void WriteTemps() { if(operand is DestinationOperand dest) { - if(dest.RegisterType == RegisterType.Temp) + if(dest.RegisterType == RegisterType.Temp + || (_shader.Type == ShaderType.Vertex && dest.RegisterType == RegisterType.Addr)) { - var registerKey = new RegisterKey(dest.RegisterType, dest.RegisterNumber); if(!tempRegisters.ContainsKey(registerKey)) { var reg = new RegisterDeclaration(registerKey); - _registers._registerDeclarations[registerKey] = reg; + _registers.RegisterDeclarations[registerKey] = reg; tempRegisters[registerKey] = (int)inst.GetDestinationWriteMask(); } else @@ -335,61 +569,50 @@ void WriteTemps() kv => kv.Key)) { int writeMask = group.Key; - string writeMaskName; switch(writeMask) + string writeMaskName = writeMask switch { - case 0x1: - writeMaskName = "float"; - break; - case 0x3: - writeMaskName = "float2"; - break; - case 0x7: - writeMaskName = "float3"; - break; - case 0xF: - writeMaskName = "float4"; - break; - default: - // TODO - writeMaskName = "float4"; - break; - //throw new NotImplementedException(); - } + 0x1 => "float", + 0x3 => "float2", + 0x7 => "float3", + 0xF => "float4", + _ => "float4",// TODO + }; WriteIndent(); WriteLine("{0} {1};", writeMaskName, string.Join(", ", group)); } - - } protected override void Write() { if(_shader.Type == ShaderType.Expression) { - Write($"// Writing expression"); - WriteExpression(_shader); - return; + throw new InvalidOperationException($"Expression should be written using {nameof(ExpressionHLSLWriter)} in {nameof(EffectHLSLWriter)}"); } + /*if(_shader.MajorVersion == 1) + { + WriteLine("#pragma message \"Shader Model 1.0 not supported\""); + WriteLine($"float4 {_entryPoint}(): POSITION;"); + return; + }*/ _registers = new RegisterState(_shader); - WriteConstantDeclarations(); - - if(_shader.Preshader != null) + foreach(var declaration in _registers.ConstantDeclarations) { - WriteExpression(_shader.Preshader.Shader); - } - if(_registers.MethodInputRegisters.Count > 1) - { - WriteInputStructureDeclaration(); + if(_effectWriter?.CommonConstantDeclarations.ContainsKey(declaration.Name) is true) + { + // skip common constant declarations + continue; + } + // write constant declaration + var decompiled = ConstantTypeWriter.Decompile(declaration, _shader); + var assignment = string.IsNullOrEmpty(decompiled.DefaultValue) + ? string.Empty + : $" = {decompiled.DefaultValue}"; + WriteLine($"{decompiled.Code}{decompiled.RegisterAssignmentString}{assignment};"); } - if(_registers.MethodOutputRegisters.Count > 1) - { - WriteOutputStructureDeclaration(); - } - string methodReturnType = GetMethodReturnType(); - string methodParameters = GetMethodParameters(); - string methodSemantic = GetMethodSemantic(); + ProcessMethodInputType(out var methodParameters); + ProcessMethodOutputType(out var methodReturnType, out var methodSemantic); WriteLine("{0} {1}({2}){3}", methodReturnType, _entryPoint, @@ -398,13 +621,19 @@ protected override void Write() WriteLine("{"); Indent++; + if(_shader.Preshader != null) + { + var preshader = PreshaderWriter.Decompile(_shader.Preshader, Indent, out var ctabOverride); + _registers.CtabOverride = ctabOverride; + WriteLine(preshader); + } + if(_registers.MethodOutputRegisters.Count > 1) { - var outputStructType = _shader.Type == ShaderType.Pixel ? "PS_OUT" : "VS_OUT"; WriteIndent(); - WriteLine($"{outputStructType} o;"); + WriteLine($"{methodReturnType} o;"); } - else + else if(_shader is not { Type: ShaderType.Pixel, MajorVersion: 1 }) // sm1 pixel shader use temp0 as output { var output = _registers.MethodOutputRegisters.First().Value; WriteIndent(); @@ -441,130 +670,59 @@ protected override void Write() WriteLine("}"); } - private void WriteConstantDeclarations() - { - if(_registers.ConstantDeclarations.Count != 0) - { - foreach(ConstantDeclaration declaration in _registers.ConstantDeclarations) - { - Write(declaration); - } - } - } - private void Write(ConstantDeclaration declaration) - { - Write(declaration.Type, declaration.Name); - if(!declaration.DefaultValue.All(v => v == 0)) - { - Write(" = {{ {0} }}", string.Join(", ", declaration.DefaultValue)); - } - WriteLine(";"); - WriteLine(); - } - private void Write(ConstantType type, string name, bool isStructMember = false) + private void WriteDeclarationsAsStruct(string typeName, IEnumerable declarations) { - string typeName = GetConstantTypeName(type); - WriteIndent(); - Write("{0}", typeName); - if(type.ParameterClass == ParameterClass.Struct) - { - WriteLine(""); - WriteLine("{"); - Indent++; - foreach(var member in type.Members) - { - Write(member.Type, member.Name, true); - } - Indent--; - WriteIndent(); - Write("}"); - } - Write(" {0}", name); - if(type.Elements > 1) - { - Write("[{0}]", type.Elements); - } - if(isStructMember) - { - Write(";\n"); - } - } - private void WriteInputStructureDeclaration() - { - var inputStructType = _shader.Type == ShaderType.Pixel ? "PS_IN" : "VS_IN"; - WriteLine($"struct {inputStructType}"); - WriteLine("{"); - Indent++; - foreach(var input in _registers.MethodInputRegisters.Values) - { - WriteIndent(); - WriteLine($"{input.TypeName} {input.Name} : {input.Semantic};"); - } - Indent--; - WriteLine("};"); - WriteLine(); - } - - private void WriteOutputStructureDeclaration() - { - var outputStructType = _shader.Type == ShaderType.Pixel ? "PS_OUT" : "VS_OUT"; - WriteLine($"struct {outputStructType}"); + WriteLine($"struct {typeName}"); WriteLine("{"); Indent++; - foreach(var output in _registers.MethodOutputRegisters.Values) + foreach(var register in declarations) { WriteIndent(); - WriteLine($"// {output.RegisterKey} {Operand.GetParamRegisterName(output.RegisterKey.Type, (uint)output.RegisterKey.Number)}"); - WriteIndent(); - WriteLine($"{output.TypeName} {output.Name} : {output.Semantic};"); + WriteLine($"{register.TypeName} {register.Name} : {register.Semantic};"); } Indent--; WriteLine("};"); WriteLine(); } - private string GetMethodReturnType() + private void ProcessMethodInputType(out string methodParameters) { - switch(_registers.MethodOutputRegisters.Count) + var registers = _registers.MethodInputRegisters.Values; + switch(registers.Count) { case 0: - throw new InvalidOperationException(); + methodParameters = string.Empty; + break; case 1: - return _registers.MethodOutputRegisters.Values.First().TypeName; + var input = registers.First(); + methodParameters = $"{input.TypeName} {input.Name} : {input.Semantic}"; + break; default: - return _shader.Type == ShaderType.Pixel ? "PS_OUT" : "VS_OUT"; + var inputTypeName = $"{_entryPoint}_Input"; + WriteDeclarationsAsStruct(inputTypeName, registers); + methodParameters = $"{inputTypeName} i"; + break; } } - private string GetMethodSemantic() + private void ProcessMethodOutputType(out string methodReturnType, out string methodSemantic) { - switch(_registers.MethodOutputRegisters.Count) + var registers = _registers.MethodOutputRegisters.Values; + switch(registers.Count) { case 0: throw new InvalidOperationException(); case 1: - string semantic = _registers.MethodOutputRegisters.Values.First().Semantic; - return $" : {semantic}"; + methodReturnType = registers.First().TypeName; + string semantic = registers.First().Semantic; + methodSemantic = $" : {semantic}"; + break; default: - return string.Empty; - } - } - - private string GetMethodParameters() - { - if(_registers.MethodInputRegisters.Count == 0) - { - return string.Empty; - } - else if(_registers.MethodInputRegisters.Count == 1) - { - var input = _registers.MethodInputRegisters.Values.First(); - return $"{input.TypeName} {input.Name} : {input.Semantic}"; - } - - return _shader.Type == ShaderType.Pixel - ? "PS_IN i" - : "VS_IN i"; + methodReturnType = $"{_entryPoint}_Output"; + WriteDeclarationsAsStruct(methodReturnType, registers); + methodSemantic = string.Empty; + break; + }; } private void WriteAst(HlslAst ast) @@ -607,21 +765,5 @@ private void WriteInstructionList() } WriteLine(); } - private void WriteExpression(ShaderModel shader) - { - WriteLine("void {0}Preshader(){{", _entryPoint); - Indent++; - WriteLine($"// {shader.Type}_{shader.MajorVersion}_{shader.MinorVersion}"); - foreach(var token in shader.Fxlc.Tokens) - { - WriteLine($"// {token.ToString(shader.ConstantTable, shader.Cli)}"); - } - if(shader.Prsi != null) - { - WriteLine(shader.Prsi.Dump()); - } - Indent--; - WriteLine("}"); - } } } diff --git a/src/DXDecompiler/DX9Shader/Decompiler/PreshaderWriter.cs b/src/DXDecompiler/DX9Shader/Decompiler/PreshaderWriter.cs new file mode 100644 index 00000000..6dd02a27 --- /dev/null +++ b/src/DXDecompiler/DX9Shader/Decompiler/PreshaderWriter.cs @@ -0,0 +1,60 @@ +using DXDecompiler.DX9Shader.Bytecode; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace DXDecompiler.DX9Shader.Decompiler +{ + class PreshaderWriter : FxlcHlslWriter + { + HashSet CtabOverride { get; } = new(); + PrsiToken Prsi { get; } + + + public PreshaderWriter(Preshader shader) : base(shader.Shader) + { + Prsi = shader.Shader.Prsi; + } + + public static string Decompile(Preshader shader, int indent, out HashSet ctabOverride) + { + var writer = new PreshaderWriter(shader) + { + Indent = indent + }; + ctabOverride = writer.CtabOverride; + return writer.Decompile(); + } + + protected override void Write() + { + WriteIndent(); + WriteLine("/*"); + WriteLine(string.Join("\n", Prsi.Dump().Split('\n').Select(x => new string(' ', Indent * 4) + x))); + WriteIndent(); + WriteLine("*/"); + + for(var i = 0; i < Prsi.OutputRegisterCount; ++i) + { + WriteIndent(); + WriteLine($"float4 expr{i + Prsi.OutputRegisterOffset};"); + } + + WriteIndent(); + WriteLine("{"); + Indent++; + + WriteTemporaries(); + + WriteIndent(); + WriteLine($"// {Shader.Type}_{Shader.MajorVersion}_{Shader.MinorVersion}"); + + WriteInstructions(CtabOverride); + + Indent--; + WriteIndent(); + WriteLine("}"); + } + } +} diff --git a/src/DXDecompiler/DX9Shader/Decompiler/RegisterDeclaration.cs b/src/DXDecompiler/DX9Shader/Decompiler/RegisterDeclaration.cs index 93880b4e..dd9e01f1 100644 --- a/src/DXDecompiler/DX9Shader/Decompiler/RegisterDeclaration.cs +++ b/src/DXDecompiler/DX9Shader/Decompiler/RegisterDeclaration.cs @@ -5,37 +5,38 @@ namespace DXDecompiler.DX9Shader // https://msdn.microsoft.com/en-us/library/windows/hardware/ff549176(v=vs.85).aspx public class RegisterDeclaration { - private readonly int _maskedLength; private readonly string _semantic; public RegisterDeclaration(InstructionToken declInstruction) { RegisterKey = declInstruction.GetParamRegisterKey(1); _semantic = declInstruction.GetDeclSemantic(); - _maskedLength = declInstruction.GetDestinationMaskedLength(); + MaskedLength = declInstruction.GetDestinationMaskedLength(); } - public RegisterDeclaration(RegisterKey registerKey) + public RegisterDeclaration(RegisterKey registerKey, int maskedLength = 4, string semantic = null) { RegisterType type = registerKey.Type; - _semantic = GuessSemanticByRegisterType(type); + _semantic = semantic ?? GuessSemanticByRegisterType(type); RegisterKey = registerKey; if(_semantic != null && RegisterKey.Number != 0) { _semantic += registerKey.Number; } - _maskedLength = 4; + MaskedLength = maskedLength; } public RegisterKey RegisterKey { get; } + public int MaskedLength { get; } public string Semantic => _semantic ?? throw new NotSupportedException(); public string Name => _semantic?.ToLower() ?? RegisterKey.ToString(); + public string TypeName { get { - switch(_maskedLength) + switch(MaskedLength) { case 1: return "float"; diff --git a/src/DXDecompiler/DX9Shader/Decompiler/RegisterState.cs b/src/DXDecompiler/DX9Shader/Decompiler/RegisterState.cs index 1267eb77..5c0f2b88 100644 --- a/src/DXDecompiler/DX9Shader/Decompiler/RegisterState.cs +++ b/src/DXDecompiler/DX9Shader/Decompiler/RegisterState.cs @@ -1,4 +1,4 @@ -using DXDecompiler.DX9Shader.Bytecode.Ctab; +using DXDecompiler.DX9Shader.Bytecode.Ctab; using System; using System.Collections.Generic; using System.Globalization; @@ -11,24 +11,32 @@ public sealed class RegisterState public readonly bool ColumnMajorOrder = true; private readonly CultureInfo _culture = CultureInfo.InvariantCulture; + private readonly ShaderType _shaderType; - private ICollection _constantDefinitions { get; } = new List(); - private ICollection _constantIntDefinitions { get; } = new List(); - public IDictionary _registerDeclarations = new Dictionary(); + private ICollection _constantDefinitions = new List(); + private ICollection _constantIntDefinitions = new List(); + + public IDictionary RegisterDeclarations { get; } = new Dictionary(); + public IDictionary MethodInputRegisters { get; } = new Dictionary(); + public IDictionary MethodOutputRegisters { get; } = new Dictionary(); + public ICollection ConstantDeclarations { get; private set; } + + public HashSet CtabOverride { get; set; } public RegisterState(ShaderModel shader) { + _shaderType = shader.Type; Load(shader); } - public ICollection ConstantDeclarations { get; private set; } - public IDictionary MethodInputRegisters { get; } = new Dictionary(); - public IDictionary MethodOutputRegisters { get; } = new Dictionary(); private void Load(ShaderModel shader) { ConstantDeclarations = shader.ConstantTable.ConstantDeclarations; + + var isSm1PixelShader = shader is { Type: ShaderType.Pixel, MajorVersion: 1 }; + foreach(var constantDeclaration in ConstantDeclarations) { RegisterType registerType; @@ -58,7 +66,7 @@ private void Load(ShaderModel shader) { var registerKey = new RegisterKey(registerType, constantDeclaration.RegisterIndex + r); var registerDeclaration = new RegisterDeclaration(registerKey); - _registerDeclarations.Add(registerKey, registerDeclaration); + RegisterDeclarations.Add(registerKey, registerDeclaration); } } @@ -69,7 +77,7 @@ private void Load(ShaderModel shader) var registerDeclaration = new RegisterDeclaration(instruction); RegisterKey registerKey = registerDeclaration.RegisterKey; - _registerDeclarations.Add(registerKey, registerDeclaration); + RegisterDeclarations.Add(registerKey, registerDeclaration); switch(registerKey.Type) { @@ -115,29 +123,96 @@ private void Load(ShaderModel shader) int destIndex = instruction.GetDestinationParamIndex(); RegisterType registerType = instruction.GetParamRegisterType(destIndex); var registerNumber = instruction.GetParamRegisterNumber(destIndex); + var maskedLength = instruction.GetDestinationMaskedLength(); var registerKey = new RegisterKey(registerType, registerNumber); - if(_registerDeclarations.ContainsKey(registerKey) == false) + if(!RegisterDeclarations.TryGetValue(registerKey, out var declaration) + || declaration.MaskedLength < maskedLength) { - var reg = new RegisterDeclaration(registerKey); - _registerDeclarations[registerKey] = reg; + var isTemp0 = registerKey is { Type: RegisterType.Temp, Number: 0 }; + var isSm1PixelShaderOutput = isSm1PixelShader && isTemp0; switch(registerType) - { + { case RegisterType.AttrOut: case RegisterType.ColorOut: case RegisterType.DepthOut: case RegisterType.Output: case RegisterType.RastOut: - MethodOutputRegisters[registerKey] = reg; + case RegisterType.Temp when isSm1PixelShaderOutput: + declaration = new RegisterDeclaration(registerKey, maskedLength, isSm1PixelShaderOutput ? "COLOR" : null); + RegisterDeclarations[registerKey] = declaration; + MethodOutputRegisters[registerKey] = declaration; break; - } } + } + } + if(isSm1PixelShader) + { + // SM1 shaders doesn't have dcl instruction + // so we must check through all operands + foreach(var instruction in shader.Tokens.OfType()) + { + var operandIndex = 0; + foreach(var operand in instruction.Operands) + { + var i = operandIndex++; + + var registerType = instruction.GetParamRegisterType(i); + var registerNumber = instruction.GetParamRegisterNumber(i); + var writeMask = instruction.HasDestination + ? instruction.GetDestinationWriteMask() + : ComponentFlags.X | ComponentFlags.Y | ComponentFlags.Z | ComponentFlags.W; + int registerSize; + if(operand is SourceOperand source) + { + registerSize = instruction.GetSourceSwizzleComponents(i) + .Take(instruction.GetSourceSwizzleLimit(i) ?? 4) + .Where((_, i) => i switch + { + 0 => writeMask.HasFlag(ComponentFlags.X), + 1 => writeMask.HasFlag(ComponentFlags.Y), + 2 => writeMask.HasFlag(ComponentFlags.Z), + 3 => writeMask.HasFlag(ComponentFlags.W), + _ => throw new InvalidOperationException() + }) + .Max() + 1; + } + else if(operand is DestinationOperand dest) + { + registerSize = instruction.GetDestinationMaskedLength(); + } + else + { + throw new InvalidOperationException("Operand is neither source nor dest"); + } + + var registerKey = new RegisterKey(registerType, registerNumber); + switch(registerKey.Type) + { + case RegisterType.Input: + case RegisterType.Texture when shader.Type == ShaderType.Pixel: + if(!RegisterDeclarations.TryGetValue(registerKey, out var declaration) + || declaration.MaskedLength < registerSize) + { + var semantic = registerKey.Type switch + { + RegisterType.Input => "COLOR", + RegisterType.Texture => "TEXCOORD", + _ => throw new InvalidOperationException(), + }; + declaration = new RegisterDeclaration(registerKey, registerSize, semantic); + RegisterDeclarations[registerKey] = declaration; + MethodInputRegisters[registerKey] = declaration; + } + break; + } + } } } } - public string GetDestinationName(InstructionToken instruction) + public string GetDestinationName(InstructionToken instruction, out string writeMaskName) { int destIndex = instruction.GetDestinationParamIndex(); RegisterKey registerKey = instruction.GetParamRegisterKey(destIndex); @@ -145,86 +220,103 @@ public string GetDestinationName(InstructionToken instruction) string registerName = GetRegisterName(registerKey); registerName = registerName ?? instruction.GetParamRegisterName(destIndex); var registerLength = GetRegisterFullLength(registerKey); - string writeMaskName = instruction.GetDestinationWriteMaskName(registerLength, true); + writeMaskName = instruction.GetDestinationWriteMaskName(registerLength, true); - return string.Format("{0}{1}", registerName, writeMaskName); + return registerName; } - public string GetSourceName(InstructionToken instruction, int srcIndex) + public string GetSourceName(InstructionToken instruction, int srcIndex, out string swizzle, out string modifier, out string[] literals) { string sourceRegisterName; - RegisterKey registerKey = instruction.GetParamRegisterKey(srcIndex); + var registerKey = instruction.GetParamRegisterKey(srcIndex); var registerType = instruction.GetParamRegisterType(srcIndex); + var registerNumber = instruction.GetParamRegisterNumber(srcIndex); switch(registerType) { case RegisterType.Const: case RegisterType.Const2: case RegisterType.Const3: case RegisterType.Const4: + if(CtabOverride?.Contains(registerNumber) is true) + { + swizzle = instruction.GetSourceSwizzleName(srcIndex, true); + modifier = GetModifier(instruction.GetSourceModifier(srcIndex)); + literals = null; + return $"expr{registerNumber}"; + } + goto case RegisterType.ConstInt; case RegisterType.ConstBool: case RegisterType.ConstInt: - sourceRegisterName = GetSourceConstantName(instruction, srcIndex); - if(sourceRegisterName != null) + literals = GetSourceConstantLiterals(instruction, srcIndex, out var literalsType); + if(literals != null) { - return sourceRegisterName; + swizzle = string.Empty; + modifier = "{0}"; + return literalsType; } - ParameterType parameterType; + RegisterSet registerSet; switch(registerType) { case RegisterType.Const: case RegisterType.Const2: case RegisterType.Const3: case RegisterType.Const4: - parameterType = ParameterType.Float; + registerSet = RegisterSet.Float4; break; case RegisterType.ConstBool: - parameterType = ParameterType.Bool; + registerSet = RegisterSet.Bool; break; case RegisterType.ConstInt: - parameterType = ParameterType.Int; + registerSet = RegisterSet.Int4; break; default: throw new NotImplementedException(); } - var registerNumber = instruction.GetParamRegisterNumber(srcIndex); - ConstantDeclaration decl = FindConstant(registerNumber); + var decl = FindConstant(registerSet, registerNumber); if(decl == null) { // Constant register not found in def statements nor the constant table //TODO: + swizzle = "Error"; + modifier = "{0}"; return $"Error {registerType}{registerNumber}"; //throw new NotImplementedException(); } - var totalOffset = registerNumber - decl.RegisterIndex; - var data = decl.GetRegisterTypeByOffset(totalOffset); - var offsetFromMember = registerNumber - data.RegisterIndex; - sourceRegisterName = decl.GetMemberNameByOffset(totalOffset); - if(data.Type.ParameterClass == ParameterClass.MatrixRows) + string indexer = null; + if(instruction.IsRelativeAddressMode(srcIndex)) { - sourceRegisterName = string.Format("{0}[{1}]", decl.Name, offsetFromMember); - } - else if(data.Type.ParameterClass == ParameterClass.MatrixColumns) - { - sourceRegisterName = string.Format("transpose({0})[{1}]", decl.Name, offsetFromMember); + indexer = GetSourceName(instruction, srcIndex + 1, out var indexSwizzle, out var indexModifier, out var indexLiterals); + if(indexLiterals != null) + { + throw new InvalidOperationException(); + } + // make sure index swizzle only has one component + indexSwizzle = indexSwizzle.TrimStart('.'); + indexSwizzle = "." + indexSwizzle[0]; + indexer = string.Format(indexModifier, indexer + indexSwizzle); } + sourceRegisterName = decl.GetConstantNameByRegisterNumber(registerNumber, indexer); break; default: + literals = null; sourceRegisterName = GetRegisterName(registerKey); break; } - sourceRegisterName = sourceRegisterName ?? instruction.GetParamRegisterName(srcIndex); + sourceRegisterName ??= instruction.GetParamRegisterName(srcIndex); - sourceRegisterName += instruction.GetSourceSwizzleName(srcIndex, true); - return ApplyModifier(instruction.GetSourceModifier(srcIndex), sourceRegisterName); + swizzle = instruction.GetSourceSwizzleName(srcIndex, true); + modifier = GetModifier(instruction.GetSourceModifier(srcIndex)); + return sourceRegisterName; } public uint GetRegisterFullLength(RegisterKey registerKey) { + // TODO: handle other cases as well if(registerKey.Type == RegisterType.Const) { - var constant = FindConstant(registerKey.Number); + var constant = FindConstant(RegisterSet.Float4, registerKey.Number); var data = constant.GetRegisterTypeByOffset(registerKey.Number - constant.RegisterIndex); if(data.Type.ParameterType != ParameterType.Float) { @@ -237,7 +329,7 @@ public uint GetRegisterFullLength(RegisterKey registerKey) return data.Type.Columns; } - RegisterDeclaration decl = _registerDeclarations[registerKey]; + RegisterDeclaration decl = RegisterDeclarations[registerKey]; switch(decl.TypeName) { case "float": @@ -255,46 +347,19 @@ public uint GetRegisterFullLength(RegisterKey registerKey) public string GetRegisterName(RegisterKey registerKey) { - var decl = _registerDeclarations[registerKey]; + var decl = RegisterDeclarations[registerKey]; switch(registerKey.Type) { - case RegisterType.Texture: case RegisterType.Input: + case RegisterType.Addr when _shaderType is ShaderType.Pixel: return (MethodInputRegisters.Count == 1) ? decl.Name : ("i." + decl.Name); case RegisterType.RastOut: case RegisterType.Output: case RegisterType.AttrOut: case RegisterType.ColorOut: - return (MethodOutputRegisters.Count == 1) ? decl.Name : ("o." + decl.Name); + return (MethodOutputRegisters.Count == 1) ? "out_" + decl.Name : ("o." + decl.Name); case RegisterType.Const: - var constDecl = FindConstant(registerKey.Number); - var relativeOffset = registerKey.Number - constDecl.RegisterIndex; - var name = constDecl.GetMemberNameByOffset(relativeOffset); - var data = constDecl.GetRegisterTypeByOffset(relativeOffset); - - // sanity check - var registersOccupied = data.Type.ParameterClass == ParameterClass.MatrixColumns - ? data.Type.Columns - : data.Type.Rows; - if(registersOccupied == 1 && data.RegisterIndex != registerKey.Number) - { - throw new InvalidOperationException(); - } - - switch(data.Type.ParameterType) - { - case ParameterType.Float: - if(registersOccupied == 1) - { - return name; - } - var subElement = (registerKey.Number - data.RegisterIndex).ToString(); - return ColumnMajorOrder - ? $"transpose({name})[{subElement}]" // subElement = col - : $"{name}[{subElement}]"; // subElement = row; - default: - throw new NotImplementedException(); - } + throw new NotSupportedException($"Use {nameof(GetSourceName)} instead"); case RegisterType.Sampler: ConstantDeclaration samplerDecl = FindConstant(RegisterSet.Sampler, registerKey.Number); if(samplerDecl != null) @@ -316,6 +381,7 @@ public string GetRegisterName(RegisterKey registerKey) default: throw new NotImplementedException(); } + case RegisterType.Addr when _shaderType is not ShaderType.Pixel: case RegisterType.Temp: return registerKey.ToString(); default: @@ -329,7 +395,7 @@ public ConstantDeclaration FindConstant(RegisterInputNode register) { return null; } - return FindConstant(ParameterType.Float, register.RegisterComponentKey.Number); + return FindConstant(RegisterSet.Float4, register.RegisterComponentKey.Number); } public ConstantDeclaration FindConstant(RegisterSet set, uint index) @@ -346,196 +412,139 @@ public ConstantDeclaration FindConstant(ParameterType type, uint index) c.ContainsIndex(index)); } - - public ConstantDeclaration FindConstant(uint index) - { - return ConstantDeclarations.FirstOrDefault(c => c.ContainsIndex(index)); - } - - private string GetSourceConstantName(InstructionToken instruction, int srcIndex) + private string[] GetSourceConstantLiterals(InstructionToken instruction, int srcIndex, out string type) { var registerType = instruction.GetParamRegisterType(srcIndex); var registerNumber = instruction.GetParamRegisterNumber(srcIndex); - + var swizzleLimit = instruction.GetSourceSwizzleLimit(srcIndex) ?? 4; + string[] constants; switch(registerType) { case RegisterType.ConstBool: + type = "bool"; //throw new NotImplementedException(); return null; case RegisterType.ConstInt: + type = "int"; + var constantInt = _constantIntDefinitions.FirstOrDefault(x => x.RegisterIndex == registerNumber); + if(constantInt == null) { - var constantInt = _constantIntDefinitions.FirstOrDefault(x => x.RegisterIndex == registerNumber); - if(constantInt == null) - { - return null; - } - byte[] swizzle = instruction.GetSourceSwizzleComponents(srcIndex); - uint[] constant = { - constantInt[swizzle[0]], - constantInt[swizzle[1]], - constantInt[swizzle[2]], - constantInt[swizzle[3]] }; - - switch(instruction.GetSourceModifier(srcIndex)) + return null; + } + var intSwizzles = instruction.GetSourceSwizzleComponents(srcIndex); + if(instruction.Opcode == Opcode.Rep) + { + if(intSwizzles[0] != 0) { - case SourceModifier.None: - break; - case SourceModifier.Negate: - throw new NotImplementedException(); - /* - for (int i = 0; i < 4; i++) - { - constant[i] = -constant[i]; - } - break;*/ - default: - throw new NotImplementedException(); + // https://docs.microsoft.com/en-us/windows/win32/direct3dhlsl/rep---ps + // MSDN says "i#.yzw" are unused, but it's not clear on whether if swizzles are allowed + throw new NotImplementedException("rep i# with swizzles not yet implemented"); } + swizzleLimit = 1; + } + var intValues = intSwizzles + .Take(swizzleLimit) + .Select(s => constantInt[s]) + .ToArray(); - int destLength = instruction.GetDestinationMaskLength(); - switch(destLength) + switch(instruction.GetSourceModifier(srcIndex)) + { + case SourceModifier.None: + break; + case SourceModifier.Negate: + throw new NotImplementedException(); + /* + for (int i = 0; i < 4; i++) { - case 1: - return constant[0].ToString(); - case 2: - if(constant[0] == constant[1]) - { - return constant[0].ToString(); - } - return $"int2({constant[0]}, {constant[1]})"; - case 3: - if(constant[0] == constant[1] && constant[0] == constant[2]) - { - return constant[0].ToString(); - } - return $"int3({constant[0]}, {constant[1]}, {constant[2]})"; - case 4: - if(constant[0] == constant[1] && constant[0] == constant[2] && constant[0] == constant[3]) - { - return constant[0].ToString(); - } - return $"int4({constant[0]}, {constant[1]}, {constant[2]}, {constant[3]})"; - default: - throw new InvalidOperationException(); + intValues[i] = -intValues[i]; } + break;*/ + default: + throw new NotImplementedException(); } - + constants = intValues.Select(i => i.ToString(_culture)).ToArray(); + break; case RegisterType.Const: case RegisterType.Const2: case RegisterType.Const3: case RegisterType.Const4: + type = "float"; + var constantRegister = _constantDefinitions.FirstOrDefault(x => x.RegisterIndex == registerNumber); + if(constantRegister == null) { - var constantRegister = _constantDefinitions.FirstOrDefault(x => x.RegisterIndex == registerNumber); - if(constantRegister == null) - { - return null; - } + return null; + } - byte[] swizzle = instruction.GetSourceSwizzleComponents(srcIndex); - float[] constant = { - constantRegister[swizzle[0]], - constantRegister[swizzle[1]], - constantRegister[swizzle[2]], - constantRegister[swizzle[3]] }; + var floatValues = instruction.GetSourceSwizzleComponents(srcIndex) + .Take(swizzleLimit) + .Select(s => constantRegister[s]) + .ToArray(); - switch(instruction.GetSourceModifier(srcIndex)) - { - case SourceModifier.None: - break; - case SourceModifier.Negate: - for(int i = 0; i < 4; i++) - { - constant[i] = -constant[i]; - } - break; - default: - throw new NotImplementedException(); - } - - int destLength; - if(instruction.HasDestination) - { - destLength = instruction.GetDestinationMaskLength(); - } - else - { - if(instruction.Opcode == Opcode.If || instruction.Opcode == Opcode.IfC) + switch(instruction.GetSourceModifier(srcIndex)) + { + case SourceModifier.None: + break; + case SourceModifier.Negate: + for(int i = 0; i < floatValues.Length; i++) { - // TODO + floatValues[i] = -floatValues[i]; } - destLength = 4; - } - switch(destLength) - { - case 1: - return constant[0].ToString(_culture); - case 2: - if(constant[0] == constant[1]) - { - return constant[0].ToString(_culture); - } - return string.Format("float2({0}, {1})", - constant[0].ToString(_culture), - constant[1].ToString(_culture)); - case 3: - if(constant[0] == constant[1] && constant[0] == constant[2]) - { - return constant[0].ToString(_culture); - } - return string.Format("float3({0}, {1}, {2})", - constant[0].ToString(_culture), - constant[1].ToString(_culture), - constant[2].ToString(_culture)); - case 4: - if(constant[0] == constant[1] && constant[0] == constant[2] && constant[0] == constant[3]) - { - return constant[0].ToString(_culture); - } - return string.Format("float4({0}, {1}, {2}, {3})", - constant[0].ToString(_culture), - constant[1].ToString(_culture), - constant[2].ToString(_culture), - constant[3].ToString(_culture)); - default: - throw new InvalidOperationException(); - } + break; + case SourceModifier.Abs: + for(int i = 0; i < floatValues.Length; i++) + { + floatValues[i] = Math.Abs(floatValues[i]); + } + break; + default: + throw new NotImplementedException(); } + constants = floatValues.Select(f => f.ToString(_culture)).ToArray(); + break; default: + type = null; return null; } + + if(instruction.Opcode == Opcode.If || instruction.Opcode == Opcode.IfC) + { + // TODO + } + + return constants; } - private static string ApplyModifier(SourceModifier modifier, string value) + private static string GetModifier(SourceModifier modifier) { switch(modifier) { case SourceModifier.None: - return value; + return "{0}"; case SourceModifier.Negate: - return $"-{value}"; + return "-{0}"; case SourceModifier.Bias: - return $"{value}_bias"; + return "{0}_bias"; case SourceModifier.BiasAndNegate: - return $"-{value}_bias"; + return "-{0}_bias"; case SourceModifier.Sign: - return $"{value}_bx2"; + return "{0}_bx2"; case SourceModifier.SignAndNegate: - return $"-{value}_bx2"; + return "-{0}_bx2"; case SourceModifier.Complement: throw new NotImplementedException(); case SourceModifier.X2: - return $"(2 * {value})"; + return "(2 * {0})"; case SourceModifier.X2AndNegate: - return $"(-2 * {value})"; + return "(-2 * {0})"; case SourceModifier.DivideByZ: - return $"{value}_dz"; + return "{0}_dz"; case SourceModifier.DivideByW: - return $"{value}_dw"; + return "{0}_dw"; case SourceModifier.Abs: - return $"abs({value})"; + return "abs({0})"; case SourceModifier.AbsAndNegate: - return $"-abs({value})"; + return "-abs({0})"; case SourceModifier.Not: throw new NotImplementedException(); default: diff --git a/src/DXDecompiler/DX9Shader/FX9/Parameter.cs b/src/DXDecompiler/DX9Shader/FX9/Parameter.cs index 7be1ffd4..351b1001 100644 --- a/src/DXDecompiler/DX9Shader/FX9/Parameter.cs +++ b/src/DXDecompiler/DX9Shader/FX9/Parameter.cs @@ -76,7 +76,7 @@ public uint GetSize() return 0; } } - public string GetDecleration(int indentLevel = 0) + public string GetDeclaration(int indentLevel = 0) { string arrayDecl = ""; string semanticDecl = ""; @@ -119,14 +119,29 @@ public string GetTypeName(int indentLevel = 0) sb.AppendLine("struct {"); foreach(var member in StructMembers) { - sb.AppendLine(string.Format("{0};", member.GetDecleration(indentLevel + 1))); + sb.AppendLine(string.Format("{0};", member.GetDeclaration(indentLevel + 1))); } sb.Append(indent); sb.Append("}"); } break; case ParameterClass.Object: - sb.Append(ParameterType.ToString().ToLower()); + switch(ParameterType) + { + case ParameterType.Sampler1D: + case ParameterType.Sampler2D: + case ParameterType.Sampler3D: + case ParameterType.SamplerCube: + sb.Append(ParameterType.GetDescription()); + break; + case ParameterType.PixelShader: + case ParameterType.VertexShader: + sb.Append(ParameterType.ToString()); + break; + default: + sb.Append(ParameterType.GetDescription()); + break; + } break; default: break; diff --git a/src/DXDecompiler/DXDecompiler.csproj b/src/DXDecompiler/DXDecompiler.csproj index 4d5f6d1b..203e30f8 100644 --- a/src/DXDecompiler/DXDecompiler.csproj +++ b/src/DXDecompiler/DXDecompiler.csproj @@ -1,15 +1,6 @@  - - netstandard2.0 + netstandard2.1 + 9.0 - - - AnyCPU - - - - AnyCPU - - \ No newline at end of file diff --git a/src/DXDecompiler/packages.config b/src/DXDecompiler/packages.config deleted file mode 100644 index 6e623915..00000000 --- a/src/DXDecompiler/packages.config +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/src/DXDecompilerCmd/DXDecompilerCmd.csproj b/src/DXDecompilerCmd/DXDecompilerCmd.csproj index bfa97089..3189ee87 100644 --- a/src/DXDecompilerCmd/DXDecompilerCmd.csproj +++ b/src/DXDecompilerCmd/DXDecompilerCmd.csproj @@ -1,71 +1,27 @@ - - - + - Debug - AnyCPU - {FD3DB932-913B-476C-9C11-99DA23BC4B3E} Exe - DXDecompilerCmd - DXDecompilerCmd - v4.7 - 512 true - true - v4.7.2 - 512 + net5.0 true - true - + DXDecompilerCmd + DXDecompilerCmd + Copyright © 2020 + bin\$(Configuration)\ - AnyCPU - true full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - AnyCPU pdbonly - true - bin\Release\ - TRACE - prompt - 4 - - - - - - - - + + + - - - - - + + - - - - - - {43b35c43-bd28-4731-81aa-772ee71c08b2} - DebugParser - - - {d75528f5-54f9-4e31-94f6-03c194f74cf2} - DXDecompiler - - - \ No newline at end of file diff --git a/src/DXDecompilerCmd/Program.cs b/src/DXDecompilerCmd/Program.cs index dc6c3562..dd4ce91d 100644 --- a/src/DXDecompilerCmd/Program.cs +++ b/src/DXDecompilerCmd/Program.cs @@ -1,10 +1,11 @@ -using DXDecompiler; +using DXDecompiler; using DXDecompiler.DebugParser; using DXDecompiler.DebugParser.DX9; using DXDecompiler.DebugParser.FX9; using DXDecompiler.Decompiler; using DXDecompiler.Util; using System; +using System.Diagnostics; using System.IO; namespace DXDecompilerCmd @@ -58,6 +59,8 @@ static StreamWriter GetStream(Options options) } static void Main(string[] args) { + Trace.Listeners.Add(new ConsoleTraceListener(useErrorStream: true)); + var options = new Options(); for(int i = 0; i < args.Length; i++) { @@ -89,6 +92,12 @@ static void Main(string[] args) if(string.IsNullOrEmpty(options.SourcePath)) { Console.Error.WriteLine("No source path specified"); + Console.Error.WriteLine("Usage: "); + Console.Error.WriteLine(" DXDecompilerCmd # decompile to stdout"); + Console.Error.WriteLine(" DXDecompilerCmd -O # decompile to file"); + Console.Error.WriteLine(" DXDecompilerCmd -a # disassemble to stdout"); + Console.Error.WriteLine(" DXDecompilerCmd -a -O # disassemble to file"); + Console.Error.WriteLine(" DXDecompilerCmd -h -O # generate debug HTML"); Environment.Exit(1); } @@ -146,8 +155,16 @@ static void Main(string[] args) } else if(options.Mode == DecompileMode.Decompile) { - var hlsl = DXDecompiler.DX9Shader.HlslWriter.Decompile(data); - sw.Write(hlsl); + try + { + var hlsl = DXDecompiler.DX9Shader.HlslWriter.Decompile(data); + sw.Write(hlsl); + } + catch(Exception e) when(!System.Diagnostics.Debugger.IsAttached) + { + Console.Error.WriteLine(e); + Environment.Exit(1); + } } else if(options.Mode == DecompileMode.Debug) { diff --git a/src/DXDecompilerCmd/Properties/AssemblyInfo.cs b/src/DXDecompilerCmd/Properties/AssemblyInfo.cs index 849b337f..366304fa 100644 --- a/src/DXDecompilerCmd/Properties/AssemblyInfo.cs +++ b/src/DXDecompilerCmd/Properties/AssemblyInfo.cs @@ -1,18 +1,6 @@ using System.Reflection; using System.Runtime.InteropServices; -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("DXDecompilerCmd")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("DXDecompilerCmd")] -[assembly: AssemblyCopyright("Copyright © 2020")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - // Setting ComVisible to false makes the types in this assembly not visible // to COM components. If you need to access a type in this assembly from // COM, set the ComVisible attribute to true on that type. @@ -20,16 +8,3 @@ // The following GUID is for the ID of the typelib if this project is exposed to COM [assembly: Guid("fd3db932-913b-476c-9c11-99da23bc4b3e")] - -// Version information for an assembly consists of the following four values: -// -// Major Version -// Minor Version -// Build Number -// Revision -// -// You can specify all the values or you can default the Build and Revision Numbers -// by using the '*' as shown below: -// [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/src/DXDecompilerCmd/Properties/PublishProfiles/FolderProfile.pubxml b/src/DXDecompilerCmd/Properties/PublishProfiles/FolderProfile.pubxml new file mode 100644 index 00000000..bffe74b4 --- /dev/null +++ b/src/DXDecompilerCmd/Properties/PublishProfiles/FolderProfile.pubxml @@ -0,0 +1,18 @@ + + + + + Release + Any CPU + bin\Release\net5.0\publish\ + FileSystem + net5.0 + win-x86 + true + True + False + True + + \ No newline at end of file diff --git a/src/DebugParser/DebugParser.csproj b/src/DebugParser/DebugParser.csproj index 2466ef33..0df8172f 100644 --- a/src/DebugParser/DebugParser.csproj +++ b/src/DebugParser/DebugParser.csproj @@ -1,211 +1,19 @@ - - - + - Debug - AnyCPU - {43B35C43-BD28-4731-81AA-772EE71C08B2} - Library - Properties - DebugParser - DebugParser - v4.7.2 - 512 - true + net5.0 + DebugParser + DebugParser + Copyright © 2020 + bin\$(Configuration)\ - true full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 pdbonly - true - bin\Release\ - TRACE - prompt - 4 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + True True Resources.resx @@ -213,20 +21,14 @@ - - {d75528f5-54f9-4e31-94f6-03c194f74cf2} - DXDecompiler - + - - ResXFileCodeGenerator - Resources.Designer.cs - + - + + + - - \ No newline at end of file diff --git a/src/DebugParser/Properties/AssemblyInfo.cs b/src/DebugParser/Properties/AssemblyInfo.cs index f59d1862..c3239c8b 100644 --- a/src/DebugParser/Properties/AssemblyInfo.cs +++ b/src/DebugParser/Properties/AssemblyInfo.cs @@ -1,18 +1,6 @@ using System.Reflection; using System.Runtime.InteropServices; -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("DebugParser")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("DebugParser")] -[assembly: AssemblyCopyright("Copyright © 2020")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - // Setting ComVisible to false makes the types in this assembly not visible // to COM components. If you need to access a type in this assembly from // COM, set the ComVisible attribute to true on that type. @@ -20,16 +8,3 @@ // The following GUID is for the ID of the typelib if this project is exposed to COM [assembly: Guid("43b35c43-bd28-4731-81aa-772ee71c08b2")] - -// Version information for an assembly consists of the following four values: -// -// Major Version -// Minor Version -// Build Number -// Revision -// -// You can specify all the values or you can default the Build and Revision Numbers -// by using the '*' as shown below: -// [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")]